Files
ersatztv/web/scripts/generate-openapi-types.mjs
T
2026-07-02 18:36:56 +02:00

124 lines
3.4 KiB
JavaScript

import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import process from 'node:process';
import { fileURLToPath, pathToFileURL } 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');
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');
}
export function generateTypes(document) {
const schemas = document.components?.schemas ?? {};
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('');
return `${lines.join('\n')}\n`;
}
async function main() {
const document = JSON.parse(await readFile(inputPath, 'utf8'));
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, generateTypes(document));
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
await main();
}