UNPKG

typed-openapi

Version:
217 lines (174 loc) 6.7 kB
import type { OpenAPIObject, ReferenceObject } from "openapi3-ts/oas31"; import { get } from "pastable/server"; import { Box } from "./box.ts"; import { isReferenceObject } from "./is-reference-object.ts"; import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts"; import { normalizeString } from "./string-utils.ts"; import { NameTransformOptions } from "./types.ts"; import { AnyBoxDef, GenericFactory, type LibSchemaObject } from "./types.ts"; import { topologicalSort } from "./topological-sort.ts"; import { sanitizeName } from "./sanitize-name.ts"; const autocorrectRef = (ref: string) => (ref[1] === "/" ? ref : "#/" + ref.slice(1)); const componentsWithSchemas = ["schemas", "responses", "parameters", "requestBodies", "headers"]; export type RefInfo = { /** * The (potentially autocorrected) ref * @example "#/components/schemas/MySchema" */ ref: string; /** * The name of the ref * @example "MySchema" * */ name: string; normalized: string; kind: "schemas" | "responses" | "parameters" | "requestBodies" | "headers"; }; export const createRefResolver = ( doc: OpenAPIObject, factory: GenericFactory, nameTransform?: NameTransformOptions, ) => { // both used for debugging purpose const nameByRef = new Map<string, string>(); const refByName = new Map<string, string>(); const byRef = new Map<string, RefInfo>(); const byNormalized = new Map<string, RefInfo>(); const boxByRef = new Map<string, Box<AnyBoxDef>>(); const getSchemaByRef = <T = LibSchemaObject>(ref: string) => { // #components -> #/components const correctRef = autocorrectRef(ref); const split = correctRef.split("/"); // "#/components/schemas/Something.jsonld" -> #/components/schemas const path = split.slice(1, -1).join("/")!; const normalizedPath = path.replace("#/", "").replace("#", "").replaceAll("/", "."); const map = get(doc, normalizedPath) ?? ({} as any); // "#/components/schemas/Something.jsonld" -> "Something.jsonld" const name = split[split.length - 1]!; let normalized = normalizeString(name); if (nameTransform?.transformSchemaName) { normalized = nameTransform.transformSchemaName(normalized); } normalized = sanitizeName(normalized, "schema"); nameByRef.set(correctRef, normalized); refByName.set(normalized, correctRef); const infos = { ref: correctRef, name, normalized, kind: normalizedPath.split(".")[1] as RefInfo["kind"] }; byRef.set(infos.ref, infos); byNormalized.set(infos.normalized, infos); // doc.components.schemas["Something.jsonld"] const schema = map[name] as T; if (!schema) { throw new Error(`Unresolved ref "${name}" not found in "${path}"`); } return schema; }; const getInfosByRef = (ref: string) => byRef.get(autocorrectRef(ref))!; const schemaEntries = Object.entries(doc.components ?? {}).filter(([key]) => componentsWithSchemas.includes(key)); schemaEntries.forEach(([key, component]) => { Object.keys(component).map((name) => { const ref = `#/components/${key}/${name}`; getSchemaByRef(ref); }); }); const directDependencies = new Map<string, Set<string>>(); // need to be done after all refs are resolved schemaEntries.forEach(([key, component]) => { Object.keys(component).map((name) => { const ref = `#/components/${key}/${name}`; const schema = getSchemaByRef(ref); boxByRef.set(ref, openApiSchemaToTs({ schema, ctx: { factory, refs: { getInfosByRef } as any } })); if (!directDependencies.has(ref)) { directDependencies.set(ref, new Set<string>()); } setSchemaDependencies(schema, directDependencies.get(ref)!); }); }); const transitiveDependencies = getTransitiveDependencies(directDependencies); return { get: getSchemaByRef, unwrap: <T extends ReferenceObject | {}>(component: T) => { return (isReferenceObject(component) ? getSchemaByRef(component.$ref) : component) as Exclude<T, ReferenceObject>; }, getInfosByRef: getInfosByRef, infos: byRef, /** * Get the schemas in the order they should be generated, depending on their dependencies * so that a schema is generated before the ones that depend on it */ getOrderedSchemas: () => { const schemaOrderedByDependencies = topologicalSort(transitiveDependencies).map((ref) => { const infos = getInfosByRef(ref); return [boxByRef.get(infos.ref)!, infos] as [schema: Box<AnyBoxDef>, infos: RefInfo]; }); return schemaOrderedByDependencies; }, directDependencies, transitiveDependencies, }; }; export interface RefResolver extends ReturnType<typeof createRefResolver> {} const setSchemaDependencies = (schema: LibSchemaObject, deps: Set<string>) => { const visit = (schema: LibSchemaObject | ReferenceObject): void => { if (!schema) return; if (isReferenceObject(schema)) { deps.add(schema.$ref); return; } if (schema.allOf) { for (const allOf of schema.allOf) { visit(allOf); } return; } if (schema.oneOf) { for (const oneOf of schema.oneOf) { visit(oneOf); } return; } if (schema.anyOf) { for (const anyOf of schema.anyOf) { visit(anyOf); } return; } if (schema.type === "array") { if (!schema.items) return; return void visit(schema.items); } if (schema.type === "object" || schema.properties || schema.additionalProperties) { if (schema.properties) { for (const property in schema.properties) { visit(schema.properties[property]!); } } if (schema.additionalProperties && typeof schema.additionalProperties === "object") { visit(schema.additionalProperties); } } }; visit(schema); }; const getTransitiveDependencies = (directDependencies: Map<string, Set<string>>) => { const transitiveDependencies = new Map<string, Set<string>>(); const visitedsDeepRefs = new Set<string>(); directDependencies.forEach((deps, ref) => { if (!transitiveDependencies.has(ref)) { transitiveDependencies.set(ref, new Set()); } const visit = (depRef: string) => { transitiveDependencies.get(ref)!.add(depRef); const deps = directDependencies.get(depRef); if (deps && ref !== depRef) { deps.forEach((transitive) => { const key = ref + "__" + transitive; if (visitedsDeepRefs.has(key)) return; visitedsDeepRefs.add(key); visit(transitive); }); } }; deps.forEach((dep) => visit(dep)); }); return transitiveDependencies; };