| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
import { contract } from "./contract.ts"; |
| 14 |
import { description, title, version } from "./env.ts"; |
| 15 |
import { AUDIT_ROW, DRAWING, ERROR, routes, type Schema } from "./routes.ts"; |
| 16 |
import { doorOf } from "./security.ts"; |
| 17 |
|
| 18 |
|
| 19 |
const NAMED = new Map<Schema, string>([ |
| 20 |
[DRAWING, "Drawing"], |
| 21 |
[AUDIT_ROW, "AuditRow"], |
| 22 |
[ERROR, "Error"], |
| 23 |
]); |
| 24 |
const named = (schema: Schema): Schema => { |
| 25 |
const name = NAMED.get(schema); |
| 26 |
if (name !== undefined) return { $ref: `#/components/schemas/${name}` }; |
| 27 |
return Object.fromEntries( |
| 28 |
Object.entries(schema).map(([key, value]) => [ |
| 29 |
key, |
| 30 |
typeof value === "object" && value !== null && !Array.isArray(value) |
| 31 |
? named(value as Schema) |
| 32 |
: value, |
| 33 |
]), |
| 34 |
); |
| 35 |
}; |
| 36 |
const content = (schema: Schema) => ({ |
| 37 |
"application/json": { schema: named(schema) }, |
| 38 |
}); |
| 39 |
|
| 40 |
export function openapi(): Record<string, unknown> { |
| 41 |
const paths: Record<string, Record<string, unknown>> = {}; |
| 42 |
for (const route of routes) { |
| 43 |
if (route.api === undefined) continue; |
| 44 |
const { summary, body, answer, status, query } = route.api; |
| 45 |
const path = route.path.replace(/:(\w+)/g, "{$1}"); |
| 46 |
const params = [...route.path.matchAll(/:(\w+)/g)].map(([, name]) => ({ |
| 47 |
name, |
| 48 |
in: "path", |
| 49 |
required: true, |
| 50 |
schema: { type: "integer" }, |
| 51 |
})); |
| 52 |
const door = doorOf(route.method, route.path.replace(/:\w+/g, "0")); |
| 53 |
const least = |
| 54 |
door === undefined |
| 55 |
? undefined |
| 56 |
: contract.resources[door.resource]?.can[door.verb]; |
| 57 |
paths[path] ??= {}; |
| 58 |
paths[path][route.method.toLowerCase()] = { |
| 59 |
summary, |
| 60 |
operationId: `${route.method.toLowerCase()}${path.replace(/[{}]/g, "").replace(/(^|[/._-])(\w)/g, (_, __, c: string) => c.toUpperCase())}`, |
| 61 |
...(params.length > 0 || query !== undefined |
| 62 |
? { |
| 63 |
parameters: [ |
| 64 |
...params, |
| 65 |
...Object.entries(query ?? {}).map(([name, schema]) => ({ |
| 66 |
name, |
| 67 |
in: "query", |
| 68 |
schema: named(schema), |
| 69 |
})), |
| 70 |
], |
| 71 |
} |
| 72 |
: {}), |
| 73 |
...(body === undefined |
| 74 |
? {} |
| 75 |
: { requestBody: { required: true, content: content(body) } }), |
| 76 |
responses: { |
| 77 |
[String(status ?? 200)]: { |
| 78 |
description: summary, |
| 79 |
content: content(answer), |
| 80 |
}, |
| 81 |
default: { |
| 82 |
description: "{ error, message }: why not", |
| 83 |
content: content(ERROR), |
| 84 |
}, |
| 85 |
}, |
| 86 |
...(door === undefined ? {} : { "x-contract": { ...door, least } }), |
| 87 |
}; |
| 88 |
} |
| 89 |
return { |
| 90 |
openapi: "3.1.0", |
| 91 |
info: { title, version, description }, |
| 92 |
paths, |
| 93 |
components: { |
| 94 |
schemas: Object.fromEntries( |
| 95 |
[...NAMED].map(([schema, name]) => [name, schema]), |
| 96 |
), |
| 97 |
}, |
| 98 |
}; |
| 99 |
} |
| 100 |
|
| 101 |
|
| 102 |
export const emit = (): string => `${JSON.stringify(openapi(), null, 2)}\n`; |