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%
17.6 KB · 508 lines typescript
Raw Blame History
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  "browser_fallback",54  "wait_for",55  "wait_ms",56  "wait_until",57  "javascript",58  "block_resources",59  "screenshot",60  "links",61  "referer",62  "device",63  "locale",64  "timeout",65  "format",66  "follow_redirects",67  "max_redirects",68  "max_response_bytes",69  "retries",70  "cache",71  "debug",72] as const;7374type Json = string | number | boolean | null | Json[] | { [k: string]: Json };75export type RequestBody = Record<string, Json>;7677function isEmptyRecord(v: unknown): boolean {78  return typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v as object).length === 0;79}8081/**82 * Build the JSON body that the API should receive: the URL plus every field that differs83 * from the API default. Unknown/undefined/empty values are dropped.84 */85export function buildRequestBody(request: FetchRequestInput): RequestBody {86  const out: RequestBody = { url: request.url };87  const r = request as Record<string, unknown>;88  for (const key of FIELD_ORDER) {89    if (key === "url") continue;90    const v = r[key];91    if (v === undefined || v === null || v === "") continue;92    switch (key) {93      case "method":94        if (v !== "GET") out.method = v as string;95        break;96      case "headers":97      case "cookies":98        if (!isEmptyRecord(v)) out[key] = v as Json;99        break;100      case "body":101        if (typeof v === "string" ? v.length > 0 : !isEmptyRecord(v)) out.body = v as Json;102        break;103      case "network":104        if (v !== "auto") out.network = v as string;105        break;106      case "browser":107      case "screenshot":108      case "links":109      case "debug":110        if (v === true) out[key] = true;111        break;112      case "browser_fallback":113      case "javascript":114      case "block_resources":115        if (v === false) out[key] = false;116        break;117      case "wait_until":118        if (v !== "domcontentloaded") out.wait_until = v as string;119        break;120      case "referer":121        if (v !== "auto") out.referer = v as string;122        break;123      case "timeout":124        if (v !== 30_000) out.timeout = v as number;125        break;126      case "format":127        if (v !== "html") out.format = v as string;128        break;129      case "follow_redirects":130        if (v === false) out.follow_redirects = false;131        break;132      case "max_redirects":133        if (v !== 10) out.max_redirects = v as number;134        break;135      case "cache":136        if (typeof v === "object" && (v as { enabled?: boolean }).enabled) out.cache = v as Json;137        break;138      default:139        out[key] = v as Json;140    }141  }142  return out;143}144145// ---------------------------------------------------------------------------146// Literal serializers per language147// ---------------------------------------------------------------------------148149const IND = "  ";150151function jsonPretty(v: Json): string {152  return JSON.stringify(v, null, 2);153}154155/** Escape for a double-quoted string in C-like languages (JS, Go, Java, C#, PHP double quotes). */156function cString(s: string): string {157  return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;158}159160function pyString(s: string): string {161  return cString(s);162}163164function phpString(s: string): string {165  return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;166}167168function rubyString(s: string): string {169  // `#{` interpolates inside double quotes — escape it.170  return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/#\{/g, "\\#{").replace(/\n/g, "\\n")}"`;171}172173function toPython(v: Json, depth = 0): string {174  if (v === null) return "None";175  if (typeof v === "boolean") return v ? "True" : "False";176  if (typeof v === "number") return String(v);177  if (typeof v === "string") return pyString(v);178  const pad = IND.repeat(depth + 1);179  const end = IND.repeat(depth);180  if (Array.isArray(v)) {181    if (!v.length) return "[]";182    return `[\n${v.map((x) => `${pad}${toPython(x, depth + 1)}`).join(",\n")}\n${end}]`;183  }184  const entries = Object.entries(v);185  if (!entries.length) return "{}";186  return `{\n${entries.map(([k, x]) => `${pad}${pyString(k)}: ${toPython(x, depth + 1)}`).join(",\n")}\n${end}}`;187}188189function toPhp(v: Json, depth = 0): string {190  if (v === null) return "null";191  if (typeof v === "boolean") return v ? "true" : "false";192  if (typeof v === "number") return String(v);193  if (typeof v === "string") return phpString(v);194  const pad = IND.repeat(depth + 1);195  const end = IND.repeat(depth);196  if (Array.isArray(v)) {197    if (!v.length) return "[]";198    return `[\n${v.map((x) => `${pad}${toPhp(x, depth + 1)}`).join(",\n")}\n${end}]`;199  }200  const entries = Object.entries(v);201  if (!entries.length) return "new stdClass()";202  return `[\n${entries.map(([k, x]) => `${pad}${phpString(k)} => ${toPhp(x, depth + 1)}`).join(",\n")}\n${end}]`;203}204205function toRuby(v: Json, depth = 0): string {206  if (v === null) return "nil";207  if (typeof v === "boolean") return String(v);208  if (typeof v === "number") return String(v);209  if (typeof v === "string") return rubyString(v);210  const pad = IND.repeat(depth + 1);211  const end = IND.repeat(depth);212  if (Array.isArray(v)) {213    if (!v.length) return "[]";214    return `[\n${v.map((x) => `${pad}${toRuby(x, depth + 1)}`).join(",\n")}\n${end}]`;215  }216  const entries = Object.entries(v);217  if (!entries.length) return "{}";218  return `{\n${entries.map(([k, x]) => `${pad}${rubyString(k)} => ${toRuby(x, depth + 1)}`).join(",\n")}\n${end}}`;219}220221/** Go raw string when safe, otherwise an escaped interpreted string (compact JSON). */222function goJsonLiteral(body: RequestBody): string {223  const pretty = jsonPretty(body);224  if (!pretty.includes("`")) return `\`${pretty}\``;225  return cString(JSON.stringify(body));226}227228/** Java text block: backslashes must be doubled; a `"""` sequence must be broken. */229function javaTextBlock(json: string, indent: string): string {230  const escaped = json.replace(/\\/g, "\\\\").replace(/"""/g, '\\"""');231  return `"""\n${escaped232    .split("\n")233    .map((l) => indent + l)234    .join("\n")}\n${indent}"""`;235}236237/** C# raw string literal; use more quotes than the longest run inside the content. */238function csharpRawString(json: string, indent: string): string {239  const longest = Math.max(2, ...(json.match(/"+/g) ?? []).map((m) => m.length));240  const q = '"'.repeat(longest + 1);241  return `${q}\n${json242    .split("\n")243    .map((l) => indent + l)244    .join("\n")}\n${indent}${q}`;245}246247function indentLines(s: string, indent: string): string {248  return s249    .split("\n")250    .map((l, i) => (i === 0 ? l : indent + l))251    .join("\n");252}253254// ---------------------------------------------------------------------------255// Generators256// ---------------------------------------------------------------------------257258function genCurl(body: RequestBody, o: CodegenOptions): string {259  const json = jsonPretty(body).replace(/'/g, "'\\''");260  return [`curl -X POST ${o.baseUrl}/v1/fetch \\`, `  -H "Authorization: Bearer ${o.apiKeyPlaceholder}" \\`, `  -H "Content-Type: application/json" \\`, `  -d '${json}'`].join("\n");261}262263function genJavaScript(body: RequestBody, o: CodegenOptions): string {264  return [265    `const response = await fetch(${cString(`${o.baseUrl}/v1/fetch`)}, {`,266    `  method: "POST",`,267    `  headers: {`,268    `    Authorization: ${cString(`Bearer ${o.apiKeyPlaceholder}`)},`,269    `    "Content-Type": "application/json",`,270    `  },`,271    `  body: JSON.stringify(${indentLines(jsonPretty(body), "  ")}),`,272    `});`,273    ``,274    `const result = await response.json();`,275    `if (!response.ok) throw new Error(\`\${result.error.code}: \${result.error.message}\`);`,276    ``,277    `console.log(result.status, result.metadata.network, result.metadata.duration_ms);`,278    `console.log(result.content);`,279  ].join("\n");280}281282function genTypeScriptSdk(body: RequestBody, o: CodegenOptions): string {283  return [284    `// Install from source until published: pnpm add ./packages/sdk (repo) — npm package @fetcha/sdk`,285    `import { Fetcha } from "@fetcha/sdk";`,286    ``,287    `const fetcha = new Fetcha({ apiKey: process.env.FETCHA_API_KEY ?? ${cString(o.apiKeyPlaceholder)} });`,288    ``,289    `const result = await fetcha.fetch(${indentLines(jsonPretty(body), "")});`,290    ``,291    `console.log(result.status, result.metadata.network, result.metadata.duration_ms);`,292    `console.log(result.content);`,293  ].join("\n");294}295296function genPython(body: RequestBody, o: CodegenOptions): string {297  return [298    `import requests`,299    ``,300    `response = requests.post(`,301    `    ${pyString(`${o.baseUrl}/v1/fetch`)},`,302    `    headers={`,303    `        "Authorization": ${pyString(`Bearer ${o.apiKeyPlaceholder}`)},`,304    `        "Content-Type": "application/json",`,305    `    },`,306    `    json=${indentLines(toPython(body), "    ")},`,307    `    timeout=${(typeof body.timeout === "number" ? body.timeout : 30_000) / 1000 + 15},`,308    `)`,309    `result = response.json()`,310    `if not response.ok:`,311    `    raise RuntimeError(f"{result['error']['code']}: {result['error']['message']}")`,312    ``,313    `print(result["status"], result["metadata"]["network"], result["metadata"]["duration_ms"])`,314    `print(result["content"])`,315  ].join("\n");316}317318function genPythonSdk(body: RequestBody, o: CodegenOptions): string {319  const { url, ...rest } = body;320  const kwargs = Object.entries(rest).map(([k, v]) => `    ${k}=${indentLines(toPython(v, 1), "    ")},`);321  const call = kwargs.length ? `fetcha.fetch(\n    ${pyString(url as string)},\n${kwargs.join("\n")}\n)` : `fetcha.fetch(${pyString(url as string)})`;322  return [323    `# Install from source until published: pip install ./sdk-python (repo) — PyPI package "fetcha"`,324    `import os`,325    `from fetcha import Fetcha`,326    ``,327    `fetcha = Fetcha(api_key=os.environ.get("FETCHA_API_KEY", ${pyString(o.apiKeyPlaceholder)}))`,328    ``,329    `result = ${call}`,330    ``,331    `print(result.status, result.metadata.network, result.metadata.duration_ms)`,332    `print(result.content)`,333  ].join("\n");334}335336function genGo(body: RequestBody, o: CodegenOptions): string {337  return [338    `package main`,339    ``,340    `import (`,341    `\t"bytes"`,342    `\t"fmt"`,343    `\t"io"`,344    `\t"net/http"`,345    `)`,346    ``,347    `func main() {`,348    `\tpayload := []byte(${indentLines(goJsonLiteral(body), "")})`,349    ``,350    `\treq, err := http.NewRequest("POST", ${cString(`${o.baseUrl}/v1/fetch`)}, bytes.NewBuffer(payload))`,351    `\tif err != nil {`,352    `\t\tpanic(err)`,353    `\t}`,354    `\treq.Header.Set("Authorization", ${cString(`Bearer ${o.apiKeyPlaceholder}`)})`,355    `\treq.Header.Set("Content-Type", "application/json")`,356    ``,357    `\tres, err := http.DefaultClient.Do(req)`,358    `\tif err != nil {`,359    `\t\tpanic(err)`,360    `\t}`,361    `\tdefer res.Body.Close()`,362    ``,363    `\tbody, _ := io.ReadAll(res.Body)`,364    `\tfmt.Println(res.StatusCode, res.Header.Get("X-Fetcha-Request-ID"))`,365    `\tfmt.Println(string(body))`,366    `}`,367  ].join("\n");368}369370function genPhp(body: RequestBody, o: CodegenOptions): string {371  return [372    `<?php`,373    ``,374    `$payload = ${toPhp(body)};`,375    ``,376    `$ch = curl_init(${phpString(`${o.baseUrl}/v1/fetch`)});`,377    `curl_setopt_array($ch, [`,378    `    CURLOPT_POST => true,`,379    `    CURLOPT_RETURNTRANSFER => true,`,380    `    CURLOPT_HTTPHEADER => [`,381    `        ${phpString(`Authorization: Bearer ${o.apiKeyPlaceholder}`)},`,382    `        'Content-Type: application/json',`,383    `    ],`,384    `    CURLOPT_POSTFIELDS => json_encode($payload),`,385    `]);`,386    ``,387    `$response = curl_exec($ch);`,388    `$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);`,389    `curl_close($ch);`,390    ``,391    `$result = json_decode($response, true);`,392    `if ($status >= 400) {`,393    `    throw new RuntimeException($result['error']['code'] . ': ' . $result['error']['message']);`,394    `}`,395    ``,396    `echo $result['status'], ' ', $result['metadata']['network'], PHP_EOL;`,397    `echo $result['content'];`,398  ].join("\n");399}400401function genRuby(body: RequestBody, o: CodegenOptions): string {402  return [403    `require "net/http"`,404    `require "json"`,405    ``,406    `uri = URI(${rubyString(`${o.baseUrl}/v1/fetch`)})`,407    `payload = ${toRuby(body)}`,408    ``,409    `request = Net::HTTP::Post.new(uri)`,410    `request["Authorization"] = ${rubyString(`Bearer ${o.apiKeyPlaceholder}`)}`,411    `request["Content-Type"] = "application/json"`,412    `request.body = JSON.generate(payload)`,413    ``,414    `response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", read_timeout: 150) do |http|`,415    `  http.request(request)`,416    `end`,417    ``,418    `result = JSON.parse(response.body)`,419    `raise "#{result["error"]["code"]}: #{result["error"]["message"]}" unless response.is_a?(Net::HTTPSuccess)`,420    ``,421    `puts "#{result["status"]} #{result["metadata"]["network"]} #{result["metadata"]["duration_ms"]}ms"`,422    `puts result["content"]`,423  ].join("\n");424}425426function genJava(body: RequestBody, o: CodegenOptions): string {427  return [428    `import java.net.URI;`,429    `import java.net.http.HttpClient;`,430    `import java.net.http.HttpRequest;`,431    `import java.net.http.HttpResponse;`,432    `import java.time.Duration;`,433    ``,434    `public class FetchaExample {`,435    `    public static void main(String[] args) throws Exception {`,436    `        String payload = ${javaTextBlock(jsonPretty(body), "            ")};`,437    ``,438    `        HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();`,439    `        HttpRequest request = HttpRequest.newBuilder()`,440    `            .uri(URI.create(${cString(`${o.baseUrl}/v1/fetch`)}))`,441    `            .header("Authorization", ${cString(`Bearer ${o.apiKeyPlaceholder}`)})`,442    `            .header("Content-Type", "application/json")`,443    `            .timeout(Duration.ofSeconds(150))`,444    `            .POST(HttpRequest.BodyPublishers.ofString(payload))`,445    `            .build();`,446    ``,447    `        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());`,448    `        System.out.println(response.statusCode() + " " + response.headers().firstValue("X-Fetcha-Request-ID").orElse(""));`,449    `        System.out.println(response.body());`,450    `    }`,451    `}`,452  ].join("\n");453}454455function genCSharp(body: RequestBody, o: CodegenOptions): string {456  return [457    `using System.Net.Http;`,458    `using System.Net.Http.Headers;`,459    `using System.Text;`,460    ``,461    `var payload = ${csharpRawString(jsonPretty(body), "    ")};`,462    ``,463    `using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(150) };`,464    `client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ${cString(o.apiKeyPlaceholder)});`,465    ``,466    `var response = await client.PostAsync(`,467    `    ${cString(`${o.baseUrl}/v1/fetch`)},`,468    `    new StringContent(payload, Encoding.UTF8, "application/json")`,469    `);`,470    ``,471    `var json = await response.Content.ReadAsStringAsync();`,472    `Console.WriteLine($"{(int)response.StatusCode} {response.Headers.GetValues("X-Fetcha-Request-ID").FirstOrDefault()}");`,473    `Console.WriteLine(json);`,474  ].join("\n");475}476477/** Generate a ready-to-run snippet calling `POST /v1/fetch` with the given request. */478export function generateCode(lang: CodegenLang, request: FetchRequestInput, opts: CodegenOptions): string {479  const body = buildRequestBody(request);480  const o: CodegenOptions = { apiKeyPlaceholder: opts.apiKeyPlaceholder || "YOUR_API_KEY", baseUrl: opts.baseUrl.replace(/\/$/, "") };481  switch (lang) {482    case "curl":483      return genCurl(body, o);484    case "javascript":485      return genJavaScript(body, o);486    case "typescript":487      return genTypeScriptSdk(body, o);488    case "python":489      return genPython(body, o);490    case "python-sdk":491      return genPythonSdk(body, o);492    case "go":493      return genGo(body, o);494    case "php":495      return genPhp(body, o);496    case "ruby":497      return genRuby(body, o);498    case "java":499      return genJava(body, o);500    case "csharp":501      return genCSharp(body, o);502  }503}504505export function codegenInfo(lang: CodegenLang): CodegenLangInfo {506  return CODEGEN_LANGS.find((l) => l.id === lang) ?? CODEGEN_LANGS[0]!;507}508