import type { FetchRequestInput } from "@fetcha/core"; import type { CodeLang } from "@/components/ui/code-block"; /** * Code generator for the Playground and docs. Pure: no React, no server imports. * Only non-default fields are emitted so snippets stay minimal and copy-pasteable. */ export type CodegenLang = "curl" | "javascript" | "typescript" | "python" | "python-sdk" | "go" | "php" | "ruby" | "java" | "csharp"; export interface CodegenLangInfo { id: CodegenLang; label: string; /** Highlighter language for `CodeBlock`. */ codeLang: CodeLang; /** File extension used by "save as" / download. */ ext: string; } export const CODEGEN_LANGS: readonly CodegenLangInfo[] = [ { id: "curl", label: "cURL", codeLang: "bash", ext: "sh" }, { id: "javascript", label: "JavaScript", codeLang: "javascript", ext: "js" }, { id: "typescript", label: "TypeScript SDK", codeLang: "typescript", ext: "ts" }, { id: "python", label: "Python", codeLang: "python", ext: "py" }, { id: "python-sdk", label: "Python SDK", codeLang: "python", ext: "py" }, { id: "go", label: "Go", codeLang: "go", ext: "go" }, { id: "php", label: "PHP", codeLang: "php", ext: "php" }, { id: "ruby", label: "Ruby", codeLang: "ruby", ext: "rb" }, { id: "java", label: "Java", codeLang: "java", ext: "java" }, { id: "csharp", label: "C#", codeLang: "csharp", ext: "cs" }, ] as const; export interface CodegenOptions { /** Shown in place of the real key, e.g. `fch_live_••••1234` or `YOUR_API_KEY`. */ apiKeyPlaceholder: string; /** Public base URL, e.g. `https://www.fetcha.co` (no trailing slash). */ baseUrl: string; } /** Field order used in the emitted JSON body. */ const FIELD_ORDER = [ "url", "method", "headers", "cookies", "body", "country", "region", "city", "network", "session", "browser", "browser_fallback", "wait_for", "wait_ms", "wait_until", "javascript", "block_resources", "screenshot", "links", "referer", "device", "locale", "timeout", "format", "follow_redirects", "max_redirects", "max_response_bytes", "retries", "cache", "debug", ] as const; type Json = string | number | boolean | null | Json[] | { [k: string]: Json }; export type RequestBody = Record; function isEmptyRecord(v: unknown): boolean { return typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v as object).length === 0; } /** * Build the JSON body that the API should receive: the URL plus every field that differs * from the API default. Unknown/undefined/empty values are dropped. */ export function buildRequestBody(request: FetchRequestInput): RequestBody { const out: RequestBody = { url: request.url }; const r = request as Record; for (const key of FIELD_ORDER) { if (key === "url") continue; const v = r[key]; if (v === undefined || v === null || v === "") continue; switch (key) { case "method": if (v !== "GET") out.method = v as string; break; case "headers": case "cookies": if (!isEmptyRecord(v)) out[key] = v as Json; break; case "body": if (typeof v === "string" ? v.length > 0 : !isEmptyRecord(v)) out.body = v as Json; break; case "network": if (v !== "auto") out.network = v as string; break; case "browser": case "screenshot": case "links": case "debug": if (v === true) out[key] = true; break; case "browser_fallback": case "javascript": case "block_resources": if (v === false) out[key] = false; break; case "wait_until": if (v !== "domcontentloaded") out.wait_until = v as string; break; case "referer": if (v !== "auto") out.referer = v as string; break; case "timeout": if (v !== 30_000) out.timeout = v as number; break; case "format": if (v !== "html") out.format = v as string; break; case "follow_redirects": if (v === false) out.follow_redirects = false; break; case "max_redirects": if (v !== 10) out.max_redirects = v as number; break; case "cache": if (typeof v === "object" && (v as { enabled?: boolean }).enabled) out.cache = v as Json; break; default: out[key] = v as Json; } } return out; } // --------------------------------------------------------------------------- // Literal serializers per language // --------------------------------------------------------------------------- const IND = " "; function jsonPretty(v: Json): string { return JSON.stringify(v, null, 2); } /** Escape for a double-quoted string in C-like languages (JS, Go, Java, C#, PHP double quotes). */ function cString(s: string): string { return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`; } function pyString(s: string): string { return cString(s); } function phpString(s: string): string { return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`; } function rubyString(s: string): string { // `#{` interpolates inside double quotes — escape it. return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/#\{/g, "\\#{").replace(/\n/g, "\\n")}"`; } function toPython(v: Json, depth = 0): string { if (v === null) return "None"; if (typeof v === "boolean") return v ? "True" : "False"; if (typeof v === "number") return String(v); if (typeof v === "string") return pyString(v); const pad = IND.repeat(depth + 1); const end = IND.repeat(depth); if (Array.isArray(v)) { if (!v.length) return "[]"; return `[\n${v.map((x) => `${pad}${toPython(x, depth + 1)}`).join(",\n")}\n${end}]`; } const entries = Object.entries(v); if (!entries.length) return "{}"; return `{\n${entries.map(([k, x]) => `${pad}${pyString(k)}: ${toPython(x, depth + 1)}`).join(",\n")}\n${end}}`; } function toPhp(v: Json, depth = 0): string { if (v === null) return "null"; if (typeof v === "boolean") return v ? "true" : "false"; if (typeof v === "number") return String(v); if (typeof v === "string") return phpString(v); const pad = IND.repeat(depth + 1); const end = IND.repeat(depth); if (Array.isArray(v)) { if (!v.length) return "[]"; return `[\n${v.map((x) => `${pad}${toPhp(x, depth + 1)}`).join(",\n")}\n${end}]`; } const entries = Object.entries(v); if (!entries.length) return "new stdClass()"; return `[\n${entries.map(([k, x]) => `${pad}${phpString(k)} => ${toPhp(x, depth + 1)}`).join(",\n")}\n${end}]`; } function toRuby(v: Json, depth = 0): string { if (v === null) return "nil"; if (typeof v === "boolean") return String(v); if (typeof v === "number") return String(v); if (typeof v === "string") return rubyString(v); const pad = IND.repeat(depth + 1); const end = IND.repeat(depth); if (Array.isArray(v)) { if (!v.length) return "[]"; return `[\n${v.map((x) => `${pad}${toRuby(x, depth + 1)}`).join(",\n")}\n${end}]`; } const entries = Object.entries(v); if (!entries.length) return "{}"; return `{\n${entries.map(([k, x]) => `${pad}${rubyString(k)} => ${toRuby(x, depth + 1)}`).join(",\n")}\n${end}}`; } /** Go raw string when safe, otherwise an escaped interpreted string (compact JSON). */ function goJsonLiteral(body: RequestBody): string { const pretty = jsonPretty(body); if (!pretty.includes("`")) return `\`${pretty}\``; return cString(JSON.stringify(body)); } /** Java text block: backslashes must be doubled; a `"""` sequence must be broken. */ function javaTextBlock(json: string, indent: string): string { const escaped = json.replace(/\\/g, "\\\\").replace(/"""/g, '\\"""'); return `"""\n${escaped .split("\n") .map((l) => indent + l) .join("\n")}\n${indent}"""`; } /** C# raw string literal; use more quotes than the longest run inside the content. */ function csharpRawString(json: string, indent: string): string { const longest = Math.max(2, ...(json.match(/"+/g) ?? []).map((m) => m.length)); const q = '"'.repeat(longest + 1); return `${q}\n${json .split("\n") .map((l) => indent + l) .join("\n")}\n${indent}${q}`; } function indentLines(s: string, indent: string): string { return s .split("\n") .map((l, i) => (i === 0 ? l : indent + l)) .join("\n"); } // --------------------------------------------------------------------------- // Generators // --------------------------------------------------------------------------- function genCurl(body: RequestBody, o: CodegenOptions): string { const json = jsonPretty(body).replace(/'/g, "'\\''"); return [`curl -X POST ${o.baseUrl}/v1/fetch \\`, ` -H "Authorization: Bearer ${o.apiKeyPlaceholder}" \\`, ` -H "Content-Type: application/json" \\`, ` -d '${json}'`].join("\n"); } function genJavaScript(body: RequestBody, o: CodegenOptions): string { return [ `const response = await fetch(${cString(`${o.baseUrl}/v1/fetch`)}, {`, ` method: "POST",`, ` headers: {`, ` Authorization: ${cString(`Bearer ${o.apiKeyPlaceholder}`)},`, ` "Content-Type": "application/json",`, ` },`, ` body: JSON.stringify(${indentLines(jsonPretty(body), " ")}),`, `});`, ``, `const result = await response.json();`, `if (!response.ok) throw new Error(\`\${result.error.code}: \${result.error.message}\`);`, ``, `console.log(result.status, result.metadata.network, result.metadata.duration_ms);`, `console.log(result.content);`, ].join("\n"); } function genTypeScriptSdk(body: RequestBody, o: CodegenOptions): string { return [ `// Install from source until published: pnpm add ./packages/sdk (repo) — npm package @fetcha/sdk`, `import { Fetcha } from "@fetcha/sdk";`, ``, `const fetcha = new Fetcha({ apiKey: process.env.FETCHA_API_KEY ?? ${cString(o.apiKeyPlaceholder)} });`, ``, `const result = await fetcha.fetch(${indentLines(jsonPretty(body), "")});`, ``, `console.log(result.status, result.metadata.network, result.metadata.duration_ms);`, `console.log(result.content);`, ].join("\n"); } function genPython(body: RequestBody, o: CodegenOptions): string { return [ `import requests`, ``, `response = requests.post(`, ` ${pyString(`${o.baseUrl}/v1/fetch`)},`, ` headers={`, ` "Authorization": ${pyString(`Bearer ${o.apiKeyPlaceholder}`)},`, ` "Content-Type": "application/json",`, ` },`, ` json=${indentLines(toPython(body), " ")},`, ` timeout=${(typeof body.timeout === "number" ? body.timeout : 30_000) / 1000 + 15},`, `)`, `result = response.json()`, `if not response.ok:`, ` raise RuntimeError(f"{result['error']['code']}: {result['error']['message']}")`, ``, `print(result["status"], result["metadata"]["network"], result["metadata"]["duration_ms"])`, `print(result["content"])`, ].join("\n"); } function genPythonSdk(body: RequestBody, o: CodegenOptions): string { const { url, ...rest } = body; const kwargs = Object.entries(rest).map(([k, v]) => ` ${k}=${indentLines(toPython(v, 1), " ")},`); const call = kwargs.length ? `fetcha.fetch(\n ${pyString(url as string)},\n${kwargs.join("\n")}\n)` : `fetcha.fetch(${pyString(url as string)})`; return [ `# Install from source until published: pip install ./sdk-python (repo) — PyPI package "fetcha"`, `import os`, `from fetcha import Fetcha`, ``, `fetcha = Fetcha(api_key=os.environ.get("FETCHA_API_KEY", ${pyString(o.apiKeyPlaceholder)}))`, ``, `result = ${call}`, ``, `print(result.status, result.metadata.network, result.metadata.duration_ms)`, `print(result.content)`, ].join("\n"); } function genGo(body: RequestBody, o: CodegenOptions): string { return [ `package main`, ``, `import (`, `\t"bytes"`, `\t"fmt"`, `\t"io"`, `\t"net/http"`, `)`, ``, `func main() {`, `\tpayload := []byte(${indentLines(goJsonLiteral(body), "")})`, ``, `\treq, err := http.NewRequest("POST", ${cString(`${o.baseUrl}/v1/fetch`)}, bytes.NewBuffer(payload))`, `\tif err != nil {`, `\t\tpanic(err)`, `\t}`, `\treq.Header.Set("Authorization", ${cString(`Bearer ${o.apiKeyPlaceholder}`)})`, `\treq.Header.Set("Content-Type", "application/json")`, ``, `\tres, err := http.DefaultClient.Do(req)`, `\tif err != nil {`, `\t\tpanic(err)`, `\t}`, `\tdefer res.Body.Close()`, ``, `\tbody, _ := io.ReadAll(res.Body)`, `\tfmt.Println(res.StatusCode, res.Header.Get("X-Fetcha-Request-ID"))`, `\tfmt.Println(string(body))`, `}`, ].join("\n"); } function genPhp(body: RequestBody, o: CodegenOptions): string { return [ ` true,`, ` CURLOPT_RETURNTRANSFER => true,`, ` CURLOPT_HTTPHEADER => [`, ` ${phpString(`Authorization: Bearer ${o.apiKeyPlaceholder}`)},`, ` 'Content-Type: application/json',`, ` ],`, ` CURLOPT_POSTFIELDS => json_encode($payload),`, `]);`, ``, `$response = curl_exec($ch);`, `$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);`, `curl_close($ch);`, ``, `$result = json_decode($response, true);`, `if ($status >= 400) {`, ` throw new RuntimeException($result['error']['code'] . ': ' . $result['error']['message']);`, `}`, ``, `echo $result['status'], ' ', $result['metadata']['network'], PHP_EOL;`, `echo $result['content'];`, ].join("\n"); } function genRuby(body: RequestBody, o: CodegenOptions): string { return [ `require "net/http"`, `require "json"`, ``, `uri = URI(${rubyString(`${o.baseUrl}/v1/fetch`)})`, `payload = ${toRuby(body)}`, ``, `request = Net::HTTP::Post.new(uri)`, `request["Authorization"] = ${rubyString(`Bearer ${o.apiKeyPlaceholder}`)}`, `request["Content-Type"] = "application/json"`, `request.body = JSON.generate(payload)`, ``, `response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", read_timeout: 150) do |http|`, ` http.request(request)`, `end`, ``, `result = JSON.parse(response.body)`, `raise "#{result["error"]["code"]}: #{result["error"]["message"]}" unless response.is_a?(Net::HTTPSuccess)`, ``, `puts "#{result["status"]} #{result["metadata"]["network"]} #{result["metadata"]["duration_ms"]}ms"`, `puts result["content"]`, ].join("\n"); } function genJava(body: RequestBody, o: CodegenOptions): string { return [ `import java.net.URI;`, `import java.net.http.HttpClient;`, `import java.net.http.HttpRequest;`, `import java.net.http.HttpResponse;`, `import java.time.Duration;`, ``, `public class FetchaExample {`, ` public static void main(String[] args) throws Exception {`, ` String payload = ${javaTextBlock(jsonPretty(body), " ")};`, ``, ` HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();`, ` HttpRequest request = HttpRequest.newBuilder()`, ` .uri(URI.create(${cString(`${o.baseUrl}/v1/fetch`)}))`, ` .header("Authorization", ${cString(`Bearer ${o.apiKeyPlaceholder}`)})`, ` .header("Content-Type", "application/json")`, ` .timeout(Duration.ofSeconds(150))`, ` .POST(HttpRequest.BodyPublishers.ofString(payload))`, ` .build();`, ``, ` HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());`, ` System.out.println(response.statusCode() + " " + response.headers().firstValue("X-Fetcha-Request-ID").orElse(""));`, ` System.out.println(response.body());`, ` }`, `}`, ].join("\n"); } function genCSharp(body: RequestBody, o: CodegenOptions): string { return [ `using System.Net.Http;`, `using System.Net.Http.Headers;`, `using System.Text;`, ``, `var payload = ${csharpRawString(jsonPretty(body), " ")};`, ``, `using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(150) };`, `client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ${cString(o.apiKeyPlaceholder)});`, ``, `var response = await client.PostAsync(`, ` ${cString(`${o.baseUrl}/v1/fetch`)},`, ` new StringContent(payload, Encoding.UTF8, "application/json")`, `);`, ``, `var json = await response.Content.ReadAsStringAsync();`, `Console.WriteLine($"{(int)response.StatusCode} {response.Headers.GetValues("X-Fetcha-Request-ID").FirstOrDefault()}");`, `Console.WriteLine(json);`, ].join("\n"); } /** Generate a ready-to-run snippet calling `POST /v1/fetch` with the given request. */ export function generateCode(lang: CodegenLang, request: FetchRequestInput, opts: CodegenOptions): string { const body = buildRequestBody(request); const o: CodegenOptions = { apiKeyPlaceholder: opts.apiKeyPlaceholder || "YOUR_API_KEY", baseUrl: opts.baseUrl.replace(/\/$/, "") }; switch (lang) { case "curl": return genCurl(body, o); case "javascript": return genJavaScript(body, o); case "typescript": return genTypeScriptSdk(body, o); case "python": return genPython(body, o); case "python-sdk": return genPythonSdk(body, o); case "go": return genGo(body, o); case "php": return genPhp(body, o); case "ruby": return genRuby(body, o); case "java": return genJava(body, o); case "csharp": return genCSharp(body, o); } } export function codegenInfo(lang: CodegenLang): CodegenLangInfo { return CODEGEN_LANGS.find((l) => l.id === lang) ?? CODEGEN_LANGS[0]!; }