SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
14.0 KB · 309 lines typescript
Raw Blame History
1import type { CodeTab } from "./code-tabs";2import { DOCS_BASE_URL } from "./nav-config";34/**5 * Generates equivalent raw-HTTP samples for a Fetcha API call in the eight languages6 * shown across the docs. Samples use only standard libraries (plus `requests` in Python)7 * so they work without the SDKs, which are not yet published.8 */910type Json = null | boolean | number | string | Json[] | { [k: string]: Json };1112export interface ApiCall {13  method: "GET" | "POST" | "DELETE";14  path: string;15  body?: Record<string, Json>;16  /** Adds an Idempotency-Key header (sessions create). */17  idempotencyKey?: string;18  /** Short trailing statement per language showing how to read the response. */19  after?: Partial<Record<Lang, string>>;20}2122export type Lang = "curl" | "javascript" | "python" | "go" | "php" | "ruby" | "java" | "csharp";2324export const LANG_LABEL: Record<Lang, string> = {25  curl: "cURL",26  javascript: "JavaScript",27  python: "Python",28  go: "Go",29  php: "PHP",30  ruby: "Ruby",31  java: "Java",32  csharp: "C#",33};3435const ORDER: Lang[] = ["curl", "javascript", "python", "go", "php", "ruby", "java", "csharp"];3637/* ------------------------------------------------------------------------------------------ */38/* Literal renderers                                                                           */39/* ------------------------------------------------------------------------------------------ */4041const IND = "  ";4243function q(s: string): string {44  return JSON.stringify(s);45}4647function jsonLiteral(v: Json, depth = 0): string {48  return JSON.stringify(v, null, 2)49    .split("\n")50    .map((l, i) => (i === 0 ? l : IND.repeat(depth) + l))51    .join("\n");52}5354/** JS object literal (unquoted keys where valid). */55function jsLiteral(v: Json, depth = 0): string {56  const pad = IND.repeat(depth + 1);57  const end = IND.repeat(depth);58  if (v === null) return "null";59  if (Array.isArray(v)) return v.length ? `[${v.map((x) => jsLiteral(x, depth)).join(", ")}]` : "[]";60  if (typeof v === "object") {61    const entries = Object.entries(v);62    if (!entries.length) return "{}";63    return `{\n${entries.map(([k, x]) => `${pad}${/^[a-zA-Z_$][\w$]*$/.test(k) ? k : q(k)}: ${jsLiteral(x, depth + 1)}`).join(",\n")},\n${end}}`;64  }65  return JSON.stringify(v);66}6768function pyLiteral(v: Json, depth = 0): string {69  const pad = IND.repeat(depth + 1);70  const end = IND.repeat(depth);71  if (v === null) return "None";72  if (v === true) return "True";73  if (v === false) return "False";74  if (Array.isArray(v)) return `[${v.map((x) => pyLiteral(x, depth)).join(", ")}]`;75  if (typeof v === "object") {76    const entries = Object.entries(v);77    if (!entries.length) return "{}";78    return `{\n${entries.map(([k, x]) => `${pad}${q(k)}: ${pyLiteral(x, depth + 1)}`).join(",\n")},\n${end}}`;79  }80  return JSON.stringify(v);81}8283function rubyLiteral(v: Json, depth = 0): string {84  const pad = IND.repeat(depth + 1);85  const end = IND.repeat(depth);86  if (v === null) return "nil";87  if (Array.isArray(v)) return `[${v.map((x) => rubyLiteral(x, depth)).join(", ")}]`;88  if (typeof v === "object") {89    const entries = Object.entries(v);90    if (!entries.length) return "{}";91    return `{\n${entries.map(([k, x]) => `${pad}${q(k)} => ${rubyLiteral(x, depth + 1)}`).join(",\n")}\n${end}}`;92  }93  return JSON.stringify(v);94}9596function phpLiteral(v: Json, depth = 0): string {97  const pad = IND.repeat(depth + 1);98  const end = IND.repeat(depth);99  if (v === null) return "null";100  if (Array.isArray(v)) return `[${v.map((x) => phpLiteral(x, depth)).join(", ")}]`;101  if (typeof v === "object") {102    const entries = Object.entries(v);103    if (!entries.length) return "[]";104    return `[\n${entries.map(([k, x]) => `${pad}${phpStr(k)} => ${phpLiteral(x, depth + 1)}`).join(",\n")},\n${end}]`;105  }106  if (typeof v === "string") return phpStr(v);107  return String(v);108}109110function phpStr(s: string): string {111  return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;112}113114function goLiteral(v: Json, depth = 0): string {115  const pad = IND.repeat(depth + 1);116  const end = IND.repeat(depth);117  if (v === null) return "nil";118  if (Array.isArray(v)) return `[]any{${v.map((x) => goLiteral(x, depth)).join(", ")}}`;119  if (typeof v === "object") {120    const entries = Object.entries(v);121    if (!entries.length) return "map[string]any{}";122    return `map[string]any{\n${entries.map(([k, x]) => `${pad}${q(k)}: ${goLiteral(x, depth + 1)}`).join(",\n")},\n${end}}`;123  }124  return JSON.stringify(v);125}126127function indentBlock(s: string, spaces: number): string {128  const pad = " ".repeat(spaces);129  return s130    .split("\n")131    .map((l, i) => (i === 0 ? l : pad + l))132    .join("\n");133}134135/* ------------------------------------------------------------------------------------------ */136/* Generators                                                                                  */137/* ------------------------------------------------------------------------------------------ */138139function curl(c: ApiCall): string {140  const lines = [`curl ${DOCS_BASE_URL}${c.path} \\`];141  if (c.method !== "GET") lines.push(`  -X ${c.method} \\`);142  lines.push(`  -H "Authorization: Bearer $FETCHA_API_KEY" \\`);143  if (c.idempotencyKey) lines.push(`  -H "Idempotency-Key: ${c.idempotencyKey}" \\`);144  if (c.body) {145    lines.push(`  -H "Content-Type: application/json" \\`);146    lines.push(`  -d '${JSON.stringify(c.body, null, 2).replace(/'/g, "'\\''")}'`);147  } else {148    lines[lines.length - 1] = lines[lines.length - 1]!.replace(/ \\$/, "");149  }150  return lines.join("\n");151}152153function javascript(c: ApiCall): string {154  const headers = [`    Authorization: \`Bearer \${process.env.FETCHA_API_KEY}\`,`];155  if (c.body) headers.push(`    "Content-Type": "application/json",`);156  if (c.idempotencyKey) headers.push(`    "Idempotency-Key": ${q(c.idempotencyKey)},`);157  const parts = [`const res = await fetch(${q(`${DOCS_BASE_URL}${c.path}`)}, {`, `  method: ${q(c.method)},`, `  headers: {`, ...headers, `  },`];158  if (c.body) parts.push(`  body: JSON.stringify(${indentBlock(jsLiteral(c.body), 2)}),`);159  parts.push(`});`, ``, `const data = await res.json();`);160  parts.push(`if (!res.ok) throw new Error(\`\${data.error.code}: \${data.error.message} (\${data.error.request_id})\`);`);161  parts.push(c.after?.javascript ?? `console.log(data);`);162  return parts.join("\n");163}164165function python(c: ApiCall): string {166  const lines = [`import os`, `import requests`, ``];167  const headers = [`    "Authorization": f"Bearer {os.environ['FETCHA_API_KEY']}",`];168  if (c.body) headers.push(`    "Content-Type": "application/json",`);169  if (c.idempotencyKey) headers.push(`    "Idempotency-Key": ${q(c.idempotencyKey)},`);170  lines.push(`res = requests.${c.method.toLowerCase()}(`, `    ${q(`${DOCS_BASE_URL}${c.path}`)},`, `    headers={`, ...headers, `    },`);171  if (c.body) lines.push(`    json=${indentBlock(pyLiteral(c.body), 4)},`);172  lines.push(`)`, `data = res.json()`, `if res.status_code >= 400:`, `    err = data["error"]`, `    raise RuntimeError(f"{err['code']}: {err['message']} ({err['request_id']})")`);173  lines.push(c.after?.python ?? `print(data)`);174  return lines.join("\n");175}176177function go(c: ApiCall): string {178  const hasBody = Boolean(c.body);179  const imports = [`"encoding/json"`, `"fmt"`, `"net/http"`, `"os"`];180  if (hasBody) imports.splice(0, 0, `"bytes"`);181  const lines = [`package main`, ``, `import (`, ...imports.map((i) => `\t${i}`), `)`, ``, `func main() {`];182  if (hasBody) {183    lines.push(`\tpayload, _ := json.Marshal(${indentBlock(goLiteral(c.body as Json), 0).replace(/\n/g, "\n\t")})`);184    lines.push(`\treq, _ := http.NewRequest(${q(c.method)}, ${q(`${DOCS_BASE_URL}${c.path}`)}, bytes.NewReader(payload))`);185    lines.push(`\treq.Header.Set("Content-Type", "application/json")`);186  } else {187    lines.push(`\treq, _ := http.NewRequest(${q(c.method)}, ${q(`${DOCS_BASE_URL}${c.path}`)}, nil)`);188  }189  lines.push(`\treq.Header.Set("Authorization", "Bearer "+os.Getenv("FETCHA_API_KEY"))`);190  if (c.idempotencyKey) lines.push(`\treq.Header.Set("Idempotency-Key", ${q(c.idempotencyKey)})`);191  lines.push(192    ``,193    `\tres, err := http.DefaultClient.Do(req)`,194    `\tif err != nil {`,195    `\t\tpanic(err)`,196    `\t}`,197    `\tdefer res.Body.Close()`,198    ``,199    `\tvar data map[string]any`,200    `\tjson.NewDecoder(res.Body).Decode(&data)`,201    `\tif res.StatusCode >= 400 {`,202    `\t\tfmt.Println("error:", data["error"])`,203    `\t\treturn`,204    `\t}`,205    c.after?.go ?? `\tfmt.Println(data)`,206    `}`,207  );208  return lines.join("\n");209}210211function php(c: ApiCall): string {212  const lines = [`<?php`, ``, `$ch = curl_init(${phpStr(`${DOCS_BASE_URL}${c.path}`)});`];213  const headers = [`    'Authorization: Bearer ' . getenv('FETCHA_API_KEY'),`];214  if (c.body) headers.push(`    'Content-Type: application/json',`);215  if (c.idempotencyKey) headers.push(`    ${phpStr(`Idempotency-Key: ${c.idempotencyKey}`)},`);216  lines.push(`curl_setopt_array($ch, [`, `  CURLOPT_RETURNTRANSFER => true,`, `  CURLOPT_CUSTOMREQUEST => ${phpStr(c.method)},`, `  CURLOPT_HTTPHEADER => [`, ...headers, `  ],`);217  if (c.body) lines.push(`  CURLOPT_POSTFIELDS => json_encode(${indentBlock(phpLiteral(c.body), 2)}),`);218  lines.push(`]);`, ``, `$raw = curl_exec($ch);`, `$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);`, `curl_close($ch);`, ``, `$data = json_decode($raw, true);`, `if ($status >= 400) {`, `    throw new RuntimeException($data['error']['code'] . ': ' . $data['error']['message'] . ' (' . $data['error']['request_id'] . ')');`, `}`);219  lines.push(c.after?.php ?? `print_r($data);`);220  return lines.join("\n");221}222223function ruby(c: ApiCall): string {224  const klass = c.method === "GET" ? "Get" : c.method === "POST" ? "Post" : "Delete";225  const lines = [`require "net/http"`, `require "json"`, ``, `uri = URI(${q(`${DOCS_BASE_URL}${c.path}`)})`, `req = Net::HTTP::${klass}.new(uri)`, `req["Authorization"] = "Bearer #{ENV.fetch("FETCHA_API_KEY")}"`];226  if (c.body) {227    lines.push(`req["Content-Type"] = "application/json"`);228  }229  if (c.idempotencyKey) lines.push(`req["Idempotency-Key"] = ${q(c.idempotencyKey)}`);230  if (c.body) lines.push(`req.body = ${indentBlock(rubyLiteral(c.body), 0)}.to_json`);231  lines.push(``, `res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }`, `data = JSON.parse(res.body)`, `if res.code.to_i >= 400`, `  err = data["error"]`, `  raise "#{err["code"]}: #{err["message"]} (#{err["request_id"]})"`, `end`);232  lines.push(c.after?.ruby ?? `puts data`);233  return lines.join("\n");234}235236function java(c: ApiCall): string {237  const lines = [`import java.net.URI;`, `import java.net.http.HttpClient;`, `import java.net.http.HttpRequest;`, `import java.net.http.HttpResponse;`, ``, `public class FetchaExample {`, `  public static void main(String[] args) throws Exception {`, `    String apiKey = System.getenv("FETCHA_API_KEY");`];238  if (c.body) {239    lines.push(`    String body = """`, ...jsonLiteral(c.body).split("\n").map((l) => `        ${l}`), `        """;`);240  }241  lines.push(``, `    HttpRequest.Builder builder = HttpRequest.newBuilder()`, `        .uri(URI.create(${q(`${DOCS_BASE_URL}${c.path}`)}))`, `        .header("Authorization", "Bearer " + apiKey)`);242  if (c.body) lines.push(`        .header("Content-Type", "application/json")`);243  if (c.idempotencyKey) lines.push(`        .header("Idempotency-Key", ${q(c.idempotencyKey)})`);244  if (c.method === "GET") lines.push(`        .GET();`);245  else if (c.method === "DELETE") lines.push(`        .DELETE();`);246  else lines.push(`        .POST(HttpRequest.BodyPublishers.ofString(body));`);247  lines.push(248    ``,249    `    HttpResponse<String> res = HttpClient.newHttpClient()`,250    `        .send(builder.build(), HttpResponse.BodyHandlers.ofString());`,251    ``,252    `    // Parse res.body() with your JSON library (Jackson, Gson, ...).`,253    `    // Errors have the shape {"error": {"code", "message", "request_id"}}.`,254    `    System.out.println(res.statusCode());`,255    c.after?.java ?? `    System.out.println(res.body());`,256    `  }`,257    `}`,258  );259  return lines.join("\n");260}261262function csharp(c: ApiCall): string {263  const lines = [`using System.Net.Http;`, `using System.Net.Http.Headers;`, `using System.Text;`, `using System.Text.Json;`, ``, `var apiKey = Environment.GetEnvironmentVariable("FETCHA_API_KEY");`, `using var http = new HttpClient();`, `http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);`];264  if (c.idempotencyKey) lines.push(`http.DefaultRequestHeaders.Add("Idempotency-Key", ${q(c.idempotencyKey)});`);265  lines.push(``);266  if (c.body) {267    lines.push(`var body = """`, ...jsonLiteral(c.body).split("\n").map((l) => `    ${l}`), `    """;`, `var content = new StringContent(body, Encoding.UTF8, "application/json");`);268  }269  const url = q(`${DOCS_BASE_URL}${c.path}`);270  if (c.method === "GET") lines.push(`var res = await http.GetAsync(${url});`);271  else if (c.method === "DELETE") lines.push(`var res = await http.DeleteAsync(${url});`);272  else lines.push(`var res = await http.PostAsync(${url}, content);`);273  lines.push(274    `var json = await res.Content.ReadAsStringAsync();`,275    `using var doc = JsonDocument.Parse(json);`,276    ``,277    `if (!res.IsSuccessStatusCode)`,278    `{`,279    `    var err = doc.RootElement.GetProperty("error");`,280    `    throw new Exception($"{err.GetProperty("code")}: {err.GetProperty("message")} ({err.GetProperty("request_id")})");`,281    `}`,282    c.after?.csharp ?? `Console.WriteLine(doc.RootElement);`,283  );284  return lines.join("\n");285}286287const GEN: Record<Lang, (c: ApiCall) => string> = { curl, javascript, python, go, php, ruby, java, csharp };288289const CODE_LANG: Record<Lang, CodeTab["lang"]> = {290  curl: "bash",291  javascript: "javascript",292  python: "python",293  go: "go",294  php: "php",295  ruby: "ruby",296  java: "java",297  csharp: "csharp",298};299300/** Build the eight-language tab set for an API call. */301export function apiTabs(call: ApiCall, langs: Lang[] = ORDER): CodeTab[] {302  return langs.map((l) => ({ label: LANG_LABEL[l], lang: CODE_LANG[l], code: GEN[l](call) }));303}304305/** Convenience for the most common case: POST /v1/fetch with a body. */306export function fetchTabs(body: Record<string, Json>, after?: ApiCall["after"], langs?: Lang[]): CodeTab[] {307  return apiTabs({ method: "POST", path: "/v1/fetch", body, after }, langs);308}309