SPB Git

spb/fetcha

Public
17.1 KB · 486 lines typescript
1import type { FetchRequestInput } from "@fetcha/core";2import type { CodeLang } from "@/components/ui/code-block";34/**5 * Code generator for the Playground and docs. Pure: no React, no server imports.6 * Only non-default fields are emitted so snippets stay minimal and copy-pasteable.7 */89export type CodegenLang = "curl" | "javascript" | "typescript" | "python" | "python-sdk" | "go" | "php" | "ruby" | "java" | "csharp";1011export interface CodegenLangInfo {12  id: CodegenLang;13  label: string;14  /** Highlighter language for `CodeBlock`. */15  codeLang: CodeLang;16  /** File extension used by "save as" / download. */17  ext: string;18}1920export const CODEGEN_LANGS: readonly CodegenLangInfo[] = [21  { id: "curl", label: "cURL", codeLang: "bash", ext: "sh" },22  { id: "javascript", label: "JavaScript", codeLang: "javascript", ext: "js" },23  { id: "typescript", label: "TypeScript SDK", codeLang: "typescript", ext: "ts" },24  { id: "python", label: "Python", codeLang: "python", ext: "py" },25  { id: "python-sdk", label: "Python SDK", codeLang: "python", ext: "py" },26  { id: "go", label: "Go", codeLang: "go", ext: "go" },27  { id: "php", label: "PHP", codeLang: "php", ext: "php" },28  { id: "ruby", label: "Ruby", codeLang: "ruby", ext: "rb" },29  { id: "java", label: "Java", codeLang: "java", ext: "java" },30  { id: "csharp", label: "C#", codeLang: "csharp", ext: "cs" },31] as const;3233export interface CodegenOptions {34  /** Shown in place of the real key, e.g. `fch_live_••••1234` or `YOUR_API_KEY`. */35  apiKeyPlaceholder: string;36  /** Public base URL, e.g. `https://www.fetcha.co` (no trailing slash). */37  baseUrl: string;38}3940/** Field order used in the emitted JSON body. */41const FIELD_ORDER = [42  "url",43  "method",44  "headers",45  "cookies",46  "body",47  "country",48  "region",49  "city",50  "network",51  "session",52  "browser",53  "device",54  "locale",55  "timeout",56  "format",57  "follow_redirects",58  "max_redirects",59  "max_response_bytes",60  "retries",61  "cache",62  "debug",63] as const;6465type Json = string | number | boolean | null | Json[] | { [k: string]: Json };66export type RequestBody = Record<string, Json>;6768function isEmptyRecord(v: unknown): boolean {69  return typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v as object).length === 0;70}7172/**73 * Build the JSON body that the API should receive: the URL plus every field that differs74 * from the API default. Unknown/undefined/empty values are dropped.75 */76export function buildRequestBody(request: FetchRequestInput): RequestBody {77  const out: RequestBody = { url: request.url };78  const r = request as Record<string, unknown>;79  for (const key of FIELD_ORDER) {80    if (key === "url") continue;81    const v = r[key];82    if (v === undefined || v === null || v === "") continue;83    switch (key) {84      case "method":85        if (v !== "GET") out.method = v as string;86        break;87      case "headers":88      case "cookies":89        if (!isEmptyRecord(v)) out[key] = v as Json;90        break;91      case "body":92        if (typeof v === "string" ? v.length > 0 : !isEmptyRecord(v)) out.body = v as Json;93        break;94      case "network":95        if (v !== "auto") out.network = v as string;96        break;97      case "browser":98      case "debug":99        if (v === true) out[key] = true;100        break;101      case "timeout":102        if (v !== 30_000) out.timeout = v as number;103        break;104      case "format":105        if (v !== "html") out.format = v as string;106        break;107      case "follow_redirects":108        if (v === false) out.follow_redirects = false;109        break;110      case "max_redirects":111        if (v !== 10) out.max_redirects = v as number;112        break;113      case "cache":114        if (typeof v === "object" && (v as { enabled?: boolean }).enabled) out.cache = v as Json;115        break;116      default:117        out[key] = v as Json;118    }119  }120  return out;121}122123// ---------------------------------------------------------------------------124// Literal serializers per language125// ---------------------------------------------------------------------------126127const IND = "  ";128129function jsonPretty(v: Json): string {130  return JSON.stringify(v, null, 2);131}132133/** Escape for a double-quoted string in C-like languages (JS, Go, Java, C#, PHP double quotes). */134function cString(s: string): string {135  return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;136}137138function pyString(s: string): string {139  return cString(s);140}141142function phpString(s: string): string {143  return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;144}145146function rubyString(s: string): string {147  // `#{` interpolates inside double quotes — escape it.148  return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/#\{/g, "\\#{").replace(/\n/g, "\\n")}"`;149}150151function toPython(v: Json, depth = 0): string {152  if (v === null) return "None";153  if (typeof v === "boolean") return v ? "True" : "False";154  if (typeof v === "number") return String(v);155  if (typeof v === "string") return pyString(v);156  const pad = IND.repeat(depth + 1);157  const end = IND.repeat(depth);158  if (Array.isArray(v)) {159    if (!v.length) return "[]";160    return `[\n${v.map((x) => `${pad}${toPython(x, depth + 1)}`).join(",\n")}\n${end}]`;161  }162  const entries = Object.entries(v);163  if (!entries.length) return "{}";164  return `{\n${entries.map(([k, x]) => `${pad}${pyString(k)}: ${toPython(x, depth + 1)}`).join(",\n")}\n${end}}`;165}166167function toPhp(v: Json, depth = 0): string {168  if (v === null) return "null";169  if (typeof v === "boolean") return v ? "true" : "false";170  if (typeof v === "number") return String(v);171  if (typeof v === "string") return phpString(v);172  const pad = IND.repeat(depth + 1);173  const end = IND.repeat(depth);174  if (Array.isArray(v)) {175    if (!v.length) return "[]";176    return `[\n${v.map((x) => `${pad}${toPhp(x, depth + 1)}`).join(",\n")}\n${end}]`;177  }178  const entries = Object.entries(v);179  if (!entries.length) return "new stdClass()";180  return `[\n${entries.map(([k, x]) => `${pad}${phpString(k)} => ${toPhp(x, depth + 1)}`).join(",\n")}\n${end}]`;181}182183function toRuby(v: Json, depth = 0): string {184  if (v === null) return "nil";185  if (typeof v === "boolean") return String(v);186  if (typeof v === "number") return String(v);187  if (typeof v === "string") return rubyString(v);188  const pad = IND.repeat(depth + 1);189  const end = IND.repeat(depth);190  if (Array.isArray(v)) {191    if (!v.length) return "[]";192    return `[\n${v.map((x) => `${pad}${toRuby(x, depth + 1)}`).join(",\n")}\n${end}]`;193  }194  const entries = Object.entries(v);195  if (!entries.length) return "{}";196  return `{\n${entries.map(([k, x]) => `${pad}${rubyString(k)} => ${toRuby(x, depth + 1)}`).join(",\n")}\n${end}}`;197}198199/** Go raw string when safe, otherwise an escaped interpreted string (compact JSON). */200function goJsonLiteral(body: RequestBody): string {201  const pretty = jsonPretty(body);202  if (!pretty.includes("`")) return `\`${pretty}\``;203  return cString(JSON.stringify(body));204}205206/** Java text block: backslashes must be doubled; a `"""` sequence must be broken. */207function javaTextBlock(json: string, indent: string): string {208  const escaped = json.replace(/\\/g, "\\\\").replace(/"""/g, '\\"""');209  return `"""\n${escaped210    .split("\n")211    .map((l) => indent + l)212    .join("\n")}\n${indent}"""`;213}214215/** C# raw string literal; use more quotes than the longest run inside the content. */216function csharpRawString(json: string, indent: string): string {217  const longest = Math.max(2, ...(json.match(/"+/g) ?? []).map((m) => m.length));218  const q = '"'.repeat(longest + 1);219  return `${q}\n${json220    .split("\n")221    .map((l) => indent + l)222    .join("\n")}\n${indent}${q}`;223}224225function indentLines(s: string, indent: string): string {226  return s227    .split("\n")228    .map((l, i) => (i === 0 ? l : indent + l))229    .join("\n");230}231232// ---------------------------------------------------------------------------233// Generators234// ---------------------------------------------------------------------------235236function genCurl(body: RequestBody, o: CodegenOptions): string {237  const json = jsonPretty(body).replace(/'/g, "'\\''");238  return [`curl -X POST ${o.baseUrl}/v1/fetch \\`, `  -H "Authorization: Bearer ${o.apiKeyPlaceholder}" \\`, `  -H "Content-Type: application/json" \\`, `  -d '${json}'`].join("\n");239}240241function genJavaScript(body: RequestBody, o: CodegenOptions): string {242  return [243    `const response = await fetch(${cString(`${o.baseUrl}/v1/fetch`)}, {`,244    `  method: "POST",`,245    `  headers: {`,246    `    Authorization: ${cString(`Bearer ${o.apiKeyPlaceholder}`)},`,247    `    "Content-Type": "application/json",`,248    `  },`,249    `  body: JSON.stringify(${indentLines(jsonPretty(body), "  ")}),`,250    `});`,251    ``,252    `const result = await response.json();`,253    `if (!response.ok) throw new Error(\`\${result.error.code}: \${result.error.message}\`);`,254    ``,255    `console.log(result.status, result.metadata.network, result.metadata.duration_ms);`,256    `console.log(result.content);`,257  ].join("\n");258}259260function genTypeScriptSdk(body: RequestBody, o: CodegenOptions): string {261  return [262    `// Install from source until published: pnpm add ./packages/sdk (repo) — npm package @fetcha/sdk`,263    `import { Fetcha } from "@fetcha/sdk";`,264    ``,265    `const fetcha = new Fetcha({ apiKey: process.env.FETCHA_API_KEY ?? ${cString(o.apiKeyPlaceholder)} });`,266    ``,267    `const result = await fetcha.fetch(${indentLines(jsonPretty(body), "")});`,268    ``,269    `console.log(result.status, result.metadata.network, result.metadata.duration_ms);`,270    `console.log(result.content);`,271  ].join("\n");272}273274function genPython(body: RequestBody, o: CodegenOptions): string {275  return [276    `import requests`,277    ``,278    `response = requests.post(`,279    `    ${pyString(`${o.baseUrl}/v1/fetch`)},`,280    `    headers={`,281    `        "Authorization": ${pyString(`Bearer ${o.apiKeyPlaceholder}`)},`,282    `        "Content-Type": "application/json",`,283    `    },`,284    `    json=${indentLines(toPython(body), "    ")},`,285    `    timeout=${(typeof body.timeout === "number" ? body.timeout : 30_000) / 1000 + 15},`,286    `)`,287    `result = response.json()`,288    `if not response.ok:`,289    `    raise RuntimeError(f"{result['error']['code']}: {result['error']['message']}")`,290    ``,291    `print(result["status"], result["metadata"]["network"], result["metadata"]["duration_ms"])`,292    `print(result["content"])`,293  ].join("\n");294}295296function genPythonSdk(body: RequestBody, o: CodegenOptions): string {297  const { url, ...rest } = body;298  const kwargs = Object.entries(rest).map(([k, v]) => `    ${k}=${indentLines(toPython(v, 1), "    ")},`);299  const call = kwargs.length ? `fetcha.fetch(\n    ${pyString(url as string)},\n${kwargs.join("\n")}\n)` : `fetcha.fetch(${pyString(url as string)})`;300  return [301    `# Install from source until published: pip install ./sdk-python (repo) — PyPI package "fetcha"`,302    `import os`,303    `from fetcha import Fetcha`,304    ``,305    `fetcha = Fetcha(api_key=os.environ.get("FETCHA_API_KEY", ${pyString(o.apiKeyPlaceholder)}))`,306    ``,307    `result = ${call}`,308    ``,309    `print(result.status, result.metadata.network, result.metadata.duration_ms)`,310    `print(result.content)`,311  ].join("\n");312}313314function genGo(body: RequestBody, o: CodegenOptions): string {315  return [316    `package main`,317    ``,318    `import (`,319    `\t"bytes"`,320    `\t"fmt"`,321    `\t"io"`,322    `\t"net/http"`,323    `)`,324    ``,325    `func main() {`,326    `\tpayload := []byte(${indentLines(goJsonLiteral(body), "")})`,327    ``,328    `\treq, err := http.NewRequest("POST", ${cString(`${o.baseUrl}/v1/fetch`)}, bytes.NewBuffer(payload))`,329    `\tif err != nil {`,330    `\t\tpanic(err)`,331    `\t}`,332    `\treq.Header.Set("Authorization", ${cString(`Bearer ${o.apiKeyPlaceholder}`)})`,333    `\treq.Header.Set("Content-Type", "application/json")`,334    ``,335    `\tres, err := http.DefaultClient.Do(req)`,336    `\tif err != nil {`,337    `\t\tpanic(err)`,338    `\t}`,339    `\tdefer res.Body.Close()`,340    ``,341    `\tbody, _ := io.ReadAll(res.Body)`,342    `\tfmt.Println(res.StatusCode, res.Header.Get("X-Fetcha-Request-ID"))`,343    `\tfmt.Println(string(body))`,344    `}`,345  ].join("\n");346}347348function genPhp(body: RequestBody, o: CodegenOptions): string {349  return [350    `<?php`,351    ``,352    `$payload = ${toPhp(body)};`,353    ``,354    `$ch = curl_init(${phpString(`${o.baseUrl}/v1/fetch`)});`,355    `curl_setopt_array($ch, [`,356    `    CURLOPT_POST => true,`,357    `    CURLOPT_RETURNTRANSFER => true,`,358    `    CURLOPT_HTTPHEADER => [`,359    `        ${phpString(`Authorization: Bearer ${o.apiKeyPlaceholder}`)},`,360    `        'Content-Type: application/json',`,361    `    ],`,362    `    CURLOPT_POSTFIELDS => json_encode($payload),`,363    `]);`,364    ``,365    `$response = curl_exec($ch);`,366    `$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);`,367    `curl_close($ch);`,368    ``,369    `$result = json_decode($response, true);`,370    `if ($status >= 400) {`,371    `    throw new RuntimeException($result['error']['code'] . ': ' . $result['error']['message']);`,372    `}`,373    ``,374    `echo $result['status'], ' ', $result['metadata']['network'], PHP_EOL;`,375    `echo $result['content'];`,376  ].join("\n");377}378379function genRuby(body: RequestBody, o: CodegenOptions): string {380  return [381    `require "net/http"`,382    `require "json"`,383    ``,384    `uri = URI(${rubyString(`${o.baseUrl}/v1/fetch`)})`,385    `payload = ${toRuby(body)}`,386    ``,387    `request = Net::HTTP::Post.new(uri)`,388    `request["Authorization"] = ${rubyString(`Bearer ${o.apiKeyPlaceholder}`)}`,389    `request["Content-Type"] = "application/json"`,390    `request.body = JSON.generate(payload)`,391    ``,392    `response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", read_timeout: 150) do |http|`,393    `  http.request(request)`,394    `end`,395    ``,396    `result = JSON.parse(response.body)`,397    `raise "#{result["error"]["code"]}: #{result["error"]["message"]}" unless response.is_a?(Net::HTTPSuccess)`,398    ``,399    `puts "#{result["status"]} #{result["metadata"]["network"]} #{result["metadata"]["duration_ms"]}ms"`,400    `puts result["content"]`,401  ].join("\n");402}403404function genJava(body: RequestBody, o: CodegenOptions): string {405  return [406    `import java.net.URI;`,407    `import java.net.http.HttpClient;`,408    `import java.net.http.HttpRequest;`,409    `import java.net.http.HttpResponse;`,410    `import java.time.Duration;`,411    ``,412    `public class FetchaExample {`,413    `    public static void main(String[] args) throws Exception {`,414    `        String payload = ${javaTextBlock(jsonPretty(body), "            ")};`,415    ``,416    `        HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();`,417    `        HttpRequest request = HttpRequest.newBuilder()`,418    `            .uri(URI.create(${cString(`${o.baseUrl}/v1/fetch`)}))`,419    `            .header("Authorization", ${cString(`Bearer ${o.apiKeyPlaceholder}`)})`,420    `            .header("Content-Type", "application/json")`,421    `            .timeout(Duration.ofSeconds(150))`,422    `            .POST(HttpRequest.BodyPublishers.ofString(payload))`,423    `            .build();`,424    ``,425    `        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());`,426    `        System.out.println(response.statusCode() + " " + response.headers().firstValue("X-Fetcha-Request-ID").orElse(""));`,427    `        System.out.println(response.body());`,428    `    }`,429    `}`,430  ].join("\n");431}432433function genCSharp(body: RequestBody, o: CodegenOptions): string {434  return [435    `using System.Net.Http;`,436    `using System.Net.Http.Headers;`,437    `using System.Text;`,438    ``,439    `var payload = ${csharpRawString(jsonPretty(body), "    ")};`,440    ``,441    `using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(150) };`,442    `client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ${cString(o.apiKeyPlaceholder)});`,443    ``,444    `var response = await client.PostAsync(`,445    `    ${cString(`${o.baseUrl}/v1/fetch`)},`,446    `    new StringContent(payload, Encoding.UTF8, "application/json")`,447    `);`,448    ``,449    `var json = await response.Content.ReadAsStringAsync();`,450    `Console.WriteLine($"{(int)response.StatusCode} {response.Headers.GetValues("X-Fetcha-Request-ID").FirstOrDefault()}");`,451    `Console.WriteLine(json);`,452  ].join("\n");453}454455/** Generate a ready-to-run snippet calling `POST /v1/fetch` with the given request. */456export function generateCode(lang: CodegenLang, request: FetchRequestInput, opts: CodegenOptions): string {457  const body = buildRequestBody(request);458  const o: CodegenOptions = { apiKeyPlaceholder: opts.apiKeyPlaceholder || "YOUR_API_KEY", baseUrl: opts.baseUrl.replace(/\/$/, "") };459  switch (lang) {460    case "curl":461      return genCurl(body, o);462    case "javascript":463      return genJavaScript(body, o);464    case "typescript":465      return genTypeScriptSdk(body, o);466    case "python":467      return genPython(body, o);468    case "python-sdk":469      return genPythonSdk(body, o);470    case "go":471      return genGo(body, o);472    case "php":473      return genPhp(body, o);474    case "ruby":475      return genRuby(body, o);476    case "java":477      return genJava(body, o);478    case "csharp":479      return genCSharp(body, o);480  }481}482483export function codegenInfo(lang: CodegenLang): CodegenLangInfo {484  return CODEGEN_LANGS.find((l) => l.id === lang) ?? CODEGEN_LANGS[0]!;485}486
Files · 6b215c2d5f54167737c2d57307f22c84bc934434
Menu

Theme