import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { basename, dirname, join, relative, resolve } from "node:path"; import { buildRouter } from "@wrnexus/router"; export type SdkLanguage = "typescript" | "javascript" | "java" | "go" | "python"; export interface ApiOperation { id: string; method: string; path: string; source: string; summary?: string; description?: string; webhook?: { event: string; payloadSchema?: string; signatureHeader?: string }; } export interface ApiArtifacts { operations: ApiOperation[]; files: string[]; } function methods(file: string): string[] { const source = readFileSync(file, "utf8"); const values = [ ...source.matchAll( /export\s+(?:async\s+)?(?:const|function)\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/g, ), ].map((match) => match[1]!); if (file.endsWith(".wrn")) for (const match of source.matchAll(/\bapi\s+(GET|POST|PUT|PATCH|DELETE)\s+/g)) values.push(match[1]!); return [...new Set(values.length ? values : ["GET"])]; } function openapiPath(path: string): string { return path .replace(/\[\.\.\.([A-Za-z_$][\w$]*)\]/g, "{$1}") .replace(/\[([A-Za-z_$][\w$]*)\]/g, "{$1}") .replace(/:([A-Za-z_$][\w$]*)/g, "{$1}") .replace(/\*([A-Za-z_$][\w$]*)/g, "{$1}"); } function operationId(method: string, path: string): string { const words = `${method.toLowerCase()}-${path.replace(/^\/api\/?/, "").replace(/[^A-Za-z0-9]+/g, "-") || "root"}` .split("-") .filter(Boolean); return ( words[0]! + words .slice(1) .map((word) => word[0]!.toUpperCase() + word.slice(1)) .join("") ); } export function inspectApi(appRoot: string): ApiOperation[] { const root = resolve(appRoot); const router = buildRouter(join(root, "app")); return router.api.flatMap((route) => { const sourceText = readFileSync(route.file, "utf8"); const field = (name: string) => new RegExp(`${name}\\s*:\\s*["']([^"']+)["']`).exec(sourceText)?.[1]; const webhook = /\b(?:defineWebhook\s*\(|webhook\s*=)\s*\{/.test(sourceText) ? { event: field("event") ?? operationId("event", route.raw), payloadSchema: field("payloadSchema"), signatureHeader: field("signatureHeader"), } : undefined; return methods(route.file).map((method) => ({ id: operationId(method, route.raw), method, path: openapiPath(route.raw), source: relative(root, route.file).replace(/\\/g, "/"), summary: field("summary"), description: field("description"), webhook, })); }); } export function createOpenApi(operations: ApiOperation[], title = "WRNexus API") { const paths: Record> = {}; for (const operation of operations) { const parameters = [...operation.path.matchAll(/\{([^}]+)\}/g)].map((match) => ({ name: match[1], in: "path", required: true, schema: { type: "string" }, })); (paths[operation.path] ??= {})[operation.method.toLowerCase()] = { operationId: operation.id, summary: operation.summary ?? `${operation.method} ${operation.path}`, ...(operation.description ? { description: operation.description } : {}), tags: ["API"], parameters, responses: { "200": { description: "Successful response", content: { "application/json": { schema: {} } }, }, "400": { description: "Invalid request" }, "500": { description: "Internal error" }, }, "x-wrnexus-source": operation.source, }; } const webhooks = Object.fromEntries( operations .filter((operation) => operation.webhook) .map((operation) => [ operation.webhook!.event, { post: { summary: operation.summary ?? operation.webhook!.event, description: operation.description, parameters: operation.webhook!.signatureHeader ? [ { name: operation.webhook!.signatureHeader, in: "header", required: true, schema: { type: "string" }, }, ] : [], requestBody: { required: true, content: { "application/json": { schema: operation.webhook!.payloadSchema ? { $ref: operation.webhook!.payloadSchema } : {}, }, }, }, responses: { "200": { description: "Webhook accepted" } }, "x-wrnexus-source": operation.source, }, }, ]), ); return { openapi: "3.1.0", info: { title, version: "0.8.0" }, paths, ...(Object.keys(webhooks).length ? { webhooks } : {}), }; } function sdk(language: SdkLanguage, operations: ApiOperation[]): string { const route = (operation: ApiOperation) => operation.path.replace(/\{([^}]+)\}/g, "${encodeURIComponent(params.$1)}"); if (language === "typescript" || language === "javascript") return `${language === "typescript" ? "export type RequestOptions = { baseUrl?: string; headers?: HeadersInit };\ntype ApiEnvelope = { data?: unknown; error?: { message?: string } };\n" : ""}const request = async (${language === "typescript" ? "method: string, path: string, body: unknown, options: RequestOptions = {}" : "method, path, body, options = {}"}) => { const response = await globalThis.fetch((options.baseUrl || "") + path, { method, headers: { "content-type": "application/json", ...(options.headers || {}) }, body: body === undefined ? undefined : JSON.stringify(body) }); const value${language === "typescript" ? ": ApiEnvelope" : ""} = await response.json(); if (!response.ok) throw Object.assign(new Error(value?.error?.message || "API request failed"), { status: response.status, body: value }); return value.data ?? value; };\n${operations.map((operation) => `export const ${operation.id} = (params${language === "typescript" ? ": Record = {}" : " = {}"}, body${language === "typescript" ? ": unknown" : ""}, options${language === "typescript" ? ": RequestOptions = {}" : " = {}"}) => { ${operation.path.includes("{") ? "" : "void params; "}return request("${operation.method}", \`${route(operation)}\`, body, options); };`).join("\n")}\n`; if (language === "python") return `import json, urllib.request\n\nclass WrnexusApi:\n def __init__(self, base_url): self.base_url = base_url.rstrip('/')\n def request(self, method, path, body=None):\n data = None if body is None else json.dumps(body).encode()\n request = urllib.request.Request(self.base_url + path, data=data, method=method, headers={'content-type':'application/json'})\n with urllib.request.urlopen(request) as response: return json.load(response)\n${operations.map((operation) => ` def ${operation.id}(self, path, body=None): return self.request('${operation.method}', path, body)`).join("\n")}\n`; if (language === "go") return `package wrnexussdk\n\nimport ("bytes"; "encoding/json"; "fmt"; "net/http")\ntype Client struct { BaseURL string; HTTP *http.Client }\nfunc (c *Client) Request(method, path string, body any) (map[string]any, error) { data,_:=json.Marshal(body); req,_:=http.NewRequest(method,c.BaseURL+path,bytes.NewReader(data)); req.Header.Set("content-type","application/json"); client:=c.HTTP;if client==nil{client=http.DefaultClient};res,err:=client.Do(req);if err!=nil{return nil,err};defer res.Body.Close();if res.StatusCode>=400{return nil,fmt.Errorf("API status %d",res.StatusCode)};var out map[string]any;err=json.NewDecoder(res.Body).Decode(&out);return out,err }\n`; return `package dev.wrnexus.sdk;\nimport java.net.URI; import java.net.http.*;\npublic final class WrnexusApi { private final String baseUrl; private final HttpClient http = HttpClient.newHttpClient(); public WrnexusApi(String baseUrl){this.baseUrl=baseUrl;} public String request(String method,String path,String json)throws Exception{var request=HttpRequest.newBuilder(URI.create(baseUrl+path)).header("content-type","application/json").method(method,HttpRequest.BodyPublishers.ofString(json==null?"":json)).build();var response=http.send(request,HttpResponse.BodyHandlers.ofString());if(response.statusCode()>=400)throw new IllegalStateException("API status "+response.statusCode());return response.body();} }\n`; } function write(path: string, value: string, files: string[]): void { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, value); files.push(path); } const escapeHtml = (value: unknown) => String(value ?? "").replace( /[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]!, ); export function generateApiArtifacts( appRoot: string, languages: SdkLanguage[] = ["typescript"], ): ApiArtifacts { const root = resolve(appRoot); const output = join(root, "generated", "api"); const operations = inspectApi(root); const files: string[] = []; const spec = createOpenApi(operations, `${basename(root)} API`); write(join(output, "openapi.json"), JSON.stringify(spec, null, 2) + "\n", files); write( join(output, "postman.json"), JSON.stringify( { info: { name: spec.info.title, schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", }, item: operations.map((operation) => ({ name: operation.id, request: { method: operation.method, url: `{{baseUrl}}${operation.path}` }, })), variable: [{ key: "baseUrl", value: "http://localhost:3000" }], }, null, 2, ) + "\n", files, ); write( join(output, "index.html"), `${escapeHtml(spec.info.title)}

${escapeHtml(spec.info.title)}

OpenAPI 3.1 · ${operations.length} operations · specification

${operations.map((operation) => `

${escapeHtml(operation.method)} ${escapeHtml(operation.path)}

${escapeHtml(operation.summary ?? operation.id)}

${operation.description ? `

${escapeHtml(operation.description)}

` : ""}${operation.webhook ? `

Webhook event: ${escapeHtml(operation.webhook.event)}${operation.webhook.signatureHeader ? ` · signature: ${escapeHtml(operation.webhook.signatureHeader)}` : ""}

` : ""}${escapeHtml(operation.source)}
`).join("")}`, files, ); write( join(output, "examples.md"), `# API examples\n\n${operations.map((operation) => `## ${operation.id}\n\n\`\`\`bash\ncurl -X ${operation.method} "http://localhost:3000${operation.path}"\n\`\`\`\n`).join("\n")}`, files, ); const extensions = { typescript: "ts", javascript: "js", java: "java", go: "go", python: "py", } as const; for (const language of languages) write( join(output, "sdk", language, `wrnexus-api.${extensions[language]}`), sdk(language, operations), files, ); return { operations, files }; } export function runApiCommand(appRoot: string, kind: "api" | "sdk", args: string[]): ApiArtifacts { const supported: SdkLanguage[] = ["typescript", "javascript", "java", "go", "python"]; const language = args.find((value) => supported.includes(value as SdkLanguage)) as SdkLanguage | undefined; if (kind === "sdk" && !language) throw new Error("WRN-SDK-LANGUAGE: choose typescript, javascript, java, go, or python."); const result = generateApiArtifacts(appRoot, kind === "sdk" ? [language!] : supported); console.log( `✓ Generated ${result.operations.length} API operations and ${result.files.length} artifacts in generated/api`, ); return result; }