import { parse } from '@std/yaml'; const OPENAPI_ROOT_URL = new URL('../docs/api/v1/openapi.yaml', import.meta.url); const OUTPUT_URL = new URL('../src/lib/api/v1.openapi.json', import.meta.url); const parsedDocumentCache = new Map(); async function main(): Promise { const rootDocument = await loadDocument(OPENAPI_ROOT_URL); const bundledSpec = await resolveNode(rootDocument, OPENAPI_ROOT_URL, new Set()); await Deno.writeTextFile(OUTPUT_URL, `${JSON.stringify(bundledSpec, null, 2)}\n`); } async function loadDocument(fileUrl: URL): Promise { const cacheKey = fileUrl.href; const cachedDocument = parsedDocumentCache.get(cacheKey); if (cachedDocument !== undefined) { return cachedDocument; } const fileContent = await Deno.readTextFile(fileUrl); const parsedDocument = parse(fileContent); parsedDocumentCache.set(cacheKey, parsedDocument); return parsedDocument; } async function resolveNode( node: unknown, currentFileUrl: URL, resolutionStack: Set ): Promise { if (Array.isArray(node)) { return Promise.all(node.map((item) => resolveNode(item, currentFileUrl, resolutionStack))); } if (!isRecord(node)) { return node; } if (typeof node.$ref === 'string') { return resolveReference(node, currentFileUrl, resolutionStack); } const resolvedEntries = await Promise.all( Object.entries(node).map( async ([key, value]) => [key, await resolveNode(value, currentFileUrl, resolutionStack)] as const ) ); return Object.fromEntries(resolvedEntries); } async function resolveReference( node: Record, currentFileUrl: URL, resolutionStack: Set ): Promise { const ref = node.$ref; if (typeof ref !== 'string') { return node; } const { targetFileUrl, pointer } = splitReference(ref, currentFileUrl); const resolutionKey = `${targetFileUrl.href}#${pointer}`; if (resolutionStack.has(resolutionKey)) { throw new Error(`Circular OpenAPI reference detected: ${resolutionKey}`); } const nextResolutionStack = new Set(resolutionStack); nextResolutionStack.add(resolutionKey); const targetDocument = await loadDocument(targetFileUrl); const targetValue = getJsonPointerValue(targetDocument, pointer); if (targetValue === undefined) { throw new Error(`Unable to resolve OpenAPI reference: ${ref}`); } const resolvedTarget = await resolveNode(targetValue, targetFileUrl, nextResolutionStack); const siblingEntries = Object.entries(node).filter(([key]) => key !== '$ref'); if (siblingEntries.length === 0) { return resolvedTarget; } if (!isRecord(resolvedTarget)) { throw new Error(`Cannot merge sibling properties into non-object OpenAPI reference: ${ref}`); } return resolveNode( { ...resolvedTarget, ...Object.fromEntries(siblingEntries) }, targetFileUrl, nextResolutionStack ); } function splitReference(ref: string, currentFileUrl: URL): { targetFileUrl: URL; pointer: string } { const hashIndex = ref.indexOf('#'); const filePath = hashIndex >= 0 ? ref.slice(0, hashIndex) : ref; const pointer = hashIndex >= 0 ? ref.slice(hashIndex + 1) : ''; return { targetFileUrl: filePath ? new URL(filePath, currentFileUrl) : currentFileUrl, pointer }; } function getJsonPointerValue(document: unknown, pointer: string): unknown { if (!pointer) { return document; } const normalizedPointer = pointer.startsWith('/') ? pointer : `/${pointer}`; const segments = normalizedPointer .split('/') .slice(1) .map((segment) => decodeJsonPointerSegment(segment)); return resolvePointerSegments(document, segments); } function resolvePointerSegments(value: unknown, segments: string[]): unknown { if (segments.length === 0) { return value; } const [segment, ...rest] = segments; if (Array.isArray(value)) { const index = Number(segment); if (!Number.isInteger(index) || index < 0 || index >= value.length) { return undefined; } return resolvePointerSegments(value[index], rest); } if (!isRecord(value) || !Object.hasOwn(value, segment)) { return undefined; } return resolvePointerSegments(value[segment], rest); } function decodeJsonPointerSegment(segment: string): string { return decodeURIComponent(segment).replaceAll('~1', '/').replaceAll('~0', '~'); } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } await main();