mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-06-16 09:18:45 -04:00
159 lines
4.3 KiB
TypeScript
159 lines
4.3 KiB
TypeScript
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<string, unknown>();
|
|
|
|
async function main(): Promise<void> {
|
|
const rootDocument = await loadDocument(OPENAPI_ROOT_URL);
|
|
const bundledSpec = await resolveNode(rootDocument, OPENAPI_ROOT_URL, new Set<string>());
|
|
|
|
await Deno.writeTextFile(OUTPUT_URL, `${JSON.stringify(bundledSpec, null, 2)}\n`);
|
|
}
|
|
|
|
async function loadDocument(fileUrl: URL): Promise<unknown> {
|
|
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<string>
|
|
): Promise<unknown> {
|
|
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<string, unknown>,
|
|
currentFileUrl: URL,
|
|
resolutionStack: Set<string>
|
|
): Promise<unknown> {
|
|
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<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
await main();
|