Files
ersatztv/web/scripts/generate-openapi-types.mjs
T
2026-07-02 08:23:19 +02:00

144 lines
4.1 KiB
JavaScript

import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const webRoot = resolve(scriptDir, '..');
const repoRoot = resolve(webRoot, '..');
const inputPath = resolve(repoRoot, 'ErsatzTV/wwwroot/openapi/v1.json');
const outputPath = resolve(webRoot, 'src/api/generated/v1.d.ts');
const document = JSON.parse(await readFile(inputPath, 'utf8'));
const schemas = document.components?.schemas ?? {};
function schemaNameFromRef(ref) {
return ref.replace('#/components/schemas/', '');
}
function formatDescription(description, indent = '') {
if (!description) {
return '';
}
return `${indent}/** ${String(description).replaceAll('*/', '* /')} */\n`;
}
function typeFromSchema(schema) {
if (!schema) {
return 'unknown';
}
if (schema.$ref) {
return `components["schemas"]["${schemaNameFromRef(schema.$ref)}"]`;
}
if (schema.oneOf || schema.anyOf) {
return (schema.oneOf ?? schema.anyOf).map(typeFromSchema).join(' | ');
}
if (schema.allOf) {
return schema.allOf.map(typeFromSchema).join(' & ');
}
if (Array.isArray(schema.enum)) {
return schema.enum.map((value) => JSON.stringify(value)).join(' | ') || 'never';
}
if (Array.isArray(schema.type)) {
return schema.type.map((type) => typeFromSchema({ ...schema, type })).join(' | ');
}
switch (schema.type) {
case 'array':
return `Array<${typeFromSchema(schema.items)}>`;
case 'boolean':
return 'boolean';
case 'integer':
case 'number':
return 'number';
case 'null':
return 'null';
case 'object':
return objectTypeFromSchema(schema);
case 'string':
return 'string';
default:
if (schema.properties || schema.additionalProperties) {
return objectTypeFromSchema(schema);
}
return 'unknown';
}
}
function objectTypeFromSchema(schema) {
const properties = schema.properties ?? {};
const required = new Set(schema.required ?? []);
const lines = ['{'];
for (const [name, propertySchema] of Object.entries(properties)) {
const optional = required.has(name) ? '' : '?';
lines.push(`${formatDescription(propertySchema.description, ' ')} ${JSON.stringify(name)}${optional}: ${typeFromSchema(propertySchema)};`);
}
if (schema.additionalProperties) {
const valueType = schema.additionalProperties === true
? 'unknown'
: typeFromSchema(schema.additionalProperties);
lines.push(` [key: string]: ${valueType};`);
}
lines.push(' }');
return lines.join('\n');
}
function operationType(operation) {
if (!operation) {
return 'never';
}
const responses = operation.responses ?? {};
const successResponse = responses['200'] ?? responses['201'] ?? responses['204'];
const jsonContent = successResponse?.content?.['application/json']
?? successResponse?.content?.['text/json']
?? successResponse?.content?.['text/plain'];
if (!jsonContent?.schema) {
return 'void';
}
return typeFromSchema(jsonContent.schema);
}
const lines = [
'// This file is generated by web/scripts/generate-openapi-types.mjs.',
'// Do not edit by hand.',
'',
'export interface components {',
' schemas: {'
];
for (const [name, schema] of Object.entries(schemas).sort(([a], [b]) => a.localeCompare(b))) {
lines.push(`${formatDescription(schema.description, ' ')} ${JSON.stringify(name)}: ${typeFromSchema(schema)};`);
}
lines.push(' };');
lines.push('}');
lines.push('');
lines.push('export interface operations {');
for (const [path, methods] of Object.entries(document.paths ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
for (const [method, operation] of Object.entries(methods).sort(([a], [b]) => a.localeCompare(b))) {
const name = operation.operationId ?? `${method.toUpperCase()} ${path}`;
lines.push(` ${JSON.stringify(name)}: {`);
lines.push(` response: ${operationType(operation)};`);
lines.push(' };');
}
}
lines.push('}');
lines.push('');
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, `${lines.join('\n')}\n`);