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