import { z } from "zod"; import type { RouteDef } from "./http.ts"; type JsonSchema = Record; const toSchema = (s: z.ZodType): JsonSchema => { const { $schema: _, ...rest } = z.toJSONSchema(s, { io: "input", unrepresentable: "any" }) as JsonSchema; return rest; }; const SECURITY: Record = { none: [], session: [{ session: [] }], "token:read": [{ appToken: ["read"] }], "token:write": [{ appToken: ["write"] }], "any:read": [{ session: [] }, { appToken: ["read"] }], "any:write": [{ session: [] }, { appToken: ["write"] }], }; /** OpenAPI 3.1 document generated from the route table (paths, summaries, request schemas). */ export function buildOpenApi(routes: RouteDef[], prefix = "/api/v1"): JsonSchema { const paths: Record> = {}; for (const r of routes) { const path = `${prefix}/${r.path.replace(/:(\w+)/g, "{$1}")}`; const parameters: JsonSchema[] = [...r.path.matchAll(/:(\w+)/g)].map((m) => ({ name: m[1], in: "path", required: true, schema: { type: "string" }, })); if (r.query) { const q = toSchema(r.query) as { properties?: Record; required?: string[] }; for (const [name, schema] of Object.entries(q.properties ?? {})) { parameters.push({ name, in: "query", required: q.required?.includes(name) ?? false, schema }); } } const op: JsonSchema = { operationId: `${r.method.toLowerCase()}_${r.path.replace(/[^\w]+/g, "_")}`, summary: r.summary, security: SECURITY[r.auth], responses: { default: { description: "Success, or an error: `{ error: { code, message } }`.", }, }, }; if (parameters.length > 0) op.parameters = parameters; if (r.body) op.requestBody = { required: !r.body.safeParse(undefined).success, content: { "application/json": { schema: toSchema(r.body) } } }; if (r.multipart) { const fields = r.formFields ? (toSchema(r.formFields) as { properties?: JsonSchema; required?: string[] }) : {}; const schema = { type: "object", properties: { file: { type: "string", format: "binary" }, ...fields.properties }, required: ["file", ...(fields.required ?? [])], }; op.requestBody = { required: true, content: { "multipart/form-data": { schema } } }; } (paths[path] ??= {})[r.method.toLowerCase()] = op; } return { openapi: "3.1.0", info: { title: "STTracker API", version: "1" }, servers: [{ url: "/" }], components: { securitySchemes: { session: { type: "apiKey", in: "cookie", name: "stt_session", description: "Non-GET requests also need X-Requested-With: sttracker." }, appToken: { type: "http", scheme: "bearer", description: "App token with scopes read, write or ingest." }, }, }, paths, }; }