/** * Code sample generation. * * In-house, browser-safe generator for a small catalogue of languages * (curl / fetch / Node axios / Python requests / Go / PHP / Ruby / * Java OkHttp). We used to wrap Kong's ``httpsnippet`` here but it is * Node-only — the bundle referenced ``global`` / ``stream`` / * ``string_decoder`` and crashed in Vite dev. Writing our own keeps the * chunk small (a few kB vs ~1.5 MB) and avoids runtime polyfills. * * Adding more targets later is one function plus a row in the catalogue. */ import type { HarRequest } from './operationToHar'; // Prism language ids used by ``prism-react-renderer``. ``bash``, // ``ruby``, ``java`` and ``php`` aren't in the library's default // bundle, but our ``registerPrismLanguages`` module pulls their // grammars from ``prismjs`` at load time so they work as first-class // languages here. export const CODE_SAMPLE_TARGETS = [ { id: 'curl', label: 'cURL', prism: 'bash' }, { id: 'fetch', label: 'JavaScript', prism: 'javascript' }, { id: 'axios', label: 'Node (axios)', prism: 'javascript' }, { id: 'python', label: 'Python', prism: 'python' }, { id: 'go', label: 'Go', prism: 'go' }, { id: 'php', label: 'PHP', prism: 'php' }, { id: 'ruby', label: 'Ruby', prism: 'ruby' }, { id: 'java', label: 'Java', prism: 'java' }, ] as const; export type CodeSampleTargetId = typeof CODE_SAMPLE_TARGETS[number]['id']; // ─── Body literal helpers ───────────────────────────────────────────────────── // // Pre-formatted JSON bodies (``JSON.stringify(value, null, 2)``) carry // real newlines that we want the snippet to preserve — a one-line // escaped string like ``"{\n \"id\": 10,\n …}"`` reads as a wall of // escape sequences. Each helper below returns a multi-line string // literal in the target language's native syntax so the snippet reads // like hand-written code. /** Go raw string literal — uses backticks. Falls back to a double- * quoted escaped literal when the body itself contains a backtick. */ function goRawString(s: string): string { return s.includes('`') ? JSON.stringify(s) : `\`${s}\``; } /** Python triple-quoted string — uses ``"""``. Falls back to escaped * form when the body already contains that sequence. */ function pythonTripleQuote(s: string): string { return s.includes('"""') ? JSON.stringify(s) : `"""${s}"""`; } /** Ruby squiggly heredoc — ``<<~JSON`` style strips common leading * whitespace. Safe unless the body literally contains ``JSON`` on a * line by itself, which is vanishingly rare. */ function rubyHeredoc(s: string): string { return `<<~JSON\n${s}\nJSON`; } /** PHP heredoc — ``<< `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`) .join('&'); const sep = har.url.includes('?') ? '&' : '?'; return `${har.url}${sep}${qs}`; } function shellEscape(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'`; } function renderCurl(har: HarRequest): string { const lines: string[] = [`curl -X ${har.method} ${shellEscape(fullUrl(har))}`]; for (const h of har.headers) { lines.push(` -H ${shellEscape(`${h.name}: ${h.value}`)}`); } if (har.postData?.text) { lines.push(` -d ${shellEscape(har.postData.text)}`); } return lines.join(' \\\n'); } function jsHeadersLiteral(har: HarRequest, indent: string): string { if (!har.headers.length) return '{}'; const entries = har.headers .map((h) => `${indent} ${JSON.stringify(h.name)}: ${JSON.stringify(h.value)}`) .join(',\n'); return `{\n${entries},\n${indent}}`; } /** JS template literal — backtick string that preserves newlines and * escapes backticks / ``${`` sequences. Makes multi-line JSON body * readable inside the fetch() options. */ function jsTemplateLiteral(s: string): string { if (/[`$]/.test(s)) return JSON.stringify(s); return `\`${s}\``; } function renderFetch(har: HarRequest): string { const url = fullUrl(har); const options: string[] = [` method: ${JSON.stringify(har.method)}`]; if (har.headers.length) { options.push(` headers: ${jsHeadersLiteral(har, ' ')}`); } if (har.postData?.text) { options.push(` body: ${jsTemplateLiteral(har.postData.text)}`); } return `const response = await fetch(${JSON.stringify(url)}, {\n${options.join(',\n')},\n});\nconst data = await response.json();`; } function renderAxios(har: HarRequest): string { const url = fullUrl(har); const config: string[] = [ ` method: ${JSON.stringify(har.method.toLowerCase())}`, ` url: ${JSON.stringify(url)}`, ]; if (har.headers.length) { config.push(` headers: ${jsHeadersLiteral(har, ' ')}`); } if (har.postData?.text) { // Axios auto-serializes objects; we pass the raw JSON string as data. config.push(` data: ${har.postData.text}`); } return `import axios from 'axios';\n\nconst { data } = await axios({\n${config.join(',\n')},\n});`; } function renderPython(har: HarRequest): string { const lines: string[] = [`import requests`, ``]; lines.push(`url = ${JSON.stringify(fullUrl(har))}`); if (har.headers.length) { const headerEntries = har.headers .map((h) => ` ${JSON.stringify(h.name)}: ${JSON.stringify(h.value)}`) .join(',\n'); lines.push(`headers = {\n${headerEntries},\n}`); } if (har.postData?.text) { // ``json=payload`` on ``requests`` expects a Python object, not // a JSON string — so we leave the body as a raw dict literal // (which Python parses identically to the JSON source for the // shapes we generate). No wrapping helper needed here. lines.push(`payload = ${har.postData.text}`); } const args = [`url`]; if (har.headers.length) args.push(`headers=headers`); if (har.postData?.text) args.push(`json=payload`); lines.push(``, `response = requests.${har.method.toLowerCase()}(${args.join(', ')})`); lines.push(`data = response.json()`); return lines.join('\n'); } function renderGo(har: HarRequest): string { const url = fullUrl(har); const lines: string[] = [ `package main`, ``, `import (`, ` "fmt"`, ` "io"`, ]; if (har.postData?.text) lines.push(` "strings"`); lines.push(` "net/http"`); lines.push(`)`, ``, `func main() {`); if (har.postData?.text) { lines.push(` payload := strings.NewReader(${goRawString(har.postData.text)})`); lines.push(` req, _ := http.NewRequest(${JSON.stringify(har.method)}, ${JSON.stringify(url)}, payload)`); } else { lines.push(` req, _ := http.NewRequest(${JSON.stringify(har.method)}, ${JSON.stringify(url)}, nil)`); } for (const h of har.headers) { lines.push(` req.Header.Add(${JSON.stringify(h.name)}, ${JSON.stringify(h.value)})`); } lines.push( ``, ` res, _ := http.DefaultClient.Do(req)`, ` defer res.Body.Close()`, ` body, _ := io.ReadAll(res.Body)`, ` fmt.Println(string(body))`, `}`, ); return lines.join('\n'); } function renderPhp(har: HarRequest): string { const lines: string[] = [` ${JSON.stringify(fullUrl(har))},`); lines.push(` CURLOPT_RETURNTRANSFER => true,`); lines.push(` CURLOPT_CUSTOMREQUEST => ${JSON.stringify(har.method)},`); if (har.postData?.text) { lines.push(` CURLOPT_POSTFIELDS => ${phpHeredoc(har.postData.text)},`); } if (har.headers.length) { const headerList = har.headers .map((h) => ` ${JSON.stringify(`${h.name}: ${h.value}`)}`) .join(',\n'); lines.push(` CURLOPT_HTTPHEADER => [\n${headerList},\n ],`); } lines.push(`]);`, ``, `$response = curl_exec($curl);`, `curl_close($curl);`, `echo $response;`); return lines.join('\n'); } function renderRuby(har: HarRequest): string { const lines: string[] = [ `require 'net/http'`, `require 'uri'`, `require 'json'`, ``, `uri = URI(${JSON.stringify(fullUrl(har))})`, `http = Net::HTTP.new(uri.host, uri.port)`, `http.use_ssl = uri.scheme == 'https'`, ``, ]; const methodClass = har.method.charAt(0) + har.method.slice(1).toLowerCase(); lines.push(`request = Net::HTTP::${methodClass}.new(uri)`); for (const h of har.headers) { lines.push(`request[${JSON.stringify(h.name)}] = ${JSON.stringify(h.value)}`); } if (har.postData?.text) { lines.push(`request.body = ${rubyHeredoc(har.postData.text)}`); } lines.push(``, `response = http.request(request)`, `puts response.body`); return lines.join('\n'); } function renderJava(har: HarRequest): string { const lines: string[] = [ `OkHttpClient client = new OkHttpClient();`, ``, ]; if (har.postData?.text) { lines.push( `MediaType mediaType = MediaType.parse("application/json");`, `RequestBody body = RequestBody.create(mediaType, ${javaTextBlock(har.postData.text)});`, ``, ); } lines.push(`Request request = new Request.Builder()`); lines.push(` .url(${JSON.stringify(fullUrl(har))})`); if (har.postData?.text) { lines.push(` .method(${JSON.stringify(har.method)}, body)`); } else { lines.push(` .method(${JSON.stringify(har.method)}, null)`); } for (const h of har.headers) { lines.push(` .addHeader(${JSON.stringify(h.name)}, ${JSON.stringify(h.value)})`); } lines.push(` .build();`, ``, `Response response = client.newCall(request).execute();`); return lines.join('\n'); } const RENDERERS: Record string> = { curl: renderCurl, fetch: renderFetch, axios: renderAxios, python: renderPython, go: renderGo, php: renderPhp, ruby: renderRuby, java: renderJava, }; export function renderSnippet(har: HarRequest, targetId: CodeSampleTargetId): string | null { const renderer = RENDERERS[targetId]; if (!renderer) return null; try { return renderer(har); } catch { return null; } }