UNPKG

@samchon/openapi

Version:

OpenAPI definitions and converters for 'typia' and 'nestia'.

480 lines (465 loc) 17.2 kB
import { OpenApi } from "../OpenApi"; import { OpenApiV3 } from "../OpenApiV3"; import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker"; export namespace OpenApiV3Upgrader { export const convert = (input: OpenApiV3.IDocument): OpenApi.IDocument => ({ ...input, components: convertComponents(input.components ?? {}), paths: input.paths ? Object.fromEntries( Object.entries(input.paths) .filter(([_, v]) => v !== undefined) .map( ([key, value]) => [key, convertPathItem(input)(value)] as const, ), ) : undefined, openapi: "3.1.0", "x-samchon-emended": true, }); /* ----------------------------------------------------------- OPERATORS ----------------------------------------------------------- */ const convertPathItem = (doc: OpenApiV3.IDocument) => (pathItem: OpenApiV3.IPath): OpenApi.IPath => ({ ...(pathItem as any), ...(pathItem.get ? { get: convertOperation(doc)(pathItem)(pathItem.get) } : undefined), ...(pathItem.put ? { put: convertOperation(doc)(pathItem)(pathItem.put) } : undefined), ...(pathItem.post ? { post: convertOperation(doc)(pathItem)(pathItem.post) } : undefined), ...(pathItem.delete ? { delete: convertOperation(doc)(pathItem)(pathItem.delete) } : undefined), ...(pathItem.options ? { options: convertOperation(doc)(pathItem)(pathItem.options) } : undefined), ...(pathItem.head ? { head: convertOperation(doc)(pathItem)(pathItem.head) } : undefined), ...(pathItem.patch ? { patch: convertOperation(doc)(pathItem)(pathItem.patch) } : undefined), ...(pathItem.trace ? { trace: convertOperation(doc)(pathItem)(pathItem.trace) } : undefined), }); const convertOperation = (doc: OpenApiV3.IDocument) => (pathItem: OpenApiV3.IPath) => (input: OpenApiV3.IOperation): OpenApi.IOperation => ({ ...input, parameters: pathItem.parameters !== undefined || input.parameters !== undefined ? [...(pathItem.parameters ?? []), ...(input.parameters ?? [])] .map((p) => { if (!TypeChecker.isReference(p)) return convertParameter(doc.components ?? {})(p); const found: | Omit<OpenApiV3.IOperation.IParameter, "in"> | undefined = p.$ref.startsWith("#/components/headers/") ? doc.components?.headers?.[p.$ref.split("/").pop() ?? ""] : doc.components?.parameters?.[p.$ref.split("/").pop() ?? ""]; return found !== undefined ? convertParameter(doc.components ?? {})({ ...found, in: "header", }) : undefined!; }) .filter((_, v) => v !== undefined) : undefined, requestBody: input.requestBody ? convertRequestBody(doc)(input.requestBody) : undefined, responses: input.responses ? Object.fromEntries( Object.entries(input.responses) .filter(([_, v]) => v !== undefined) .map( ([key, value]) => [key, convertResponse(doc)(value)!] as const, ) .filter(([_, v]) => v !== undefined), ) : undefined, }); const convertParameter = (components: OpenApiV3.IComponents) => ( input: OpenApiV3.IOperation.IParameter, ): OpenApi.IOperation.IParameter => ({ ...input, schema: convertSchema(components)(input.schema), examples: input.examples ? Object.fromEntries( Object.entries(input.examples) .map(([key, value]) => [ key, TypeChecker.isReference(value) ? components.examples?.[value.$ref.split("/").pop() ?? ""] : value, ]) .filter(([_, v]) => v !== undefined), ) : undefined, }); const convertRequestBody = (doc: OpenApiV3.IDocument) => ( input: | OpenApiV3.IOperation.IRequestBody | OpenApiV3.IJsonSchema.IReference<`#/components/requestBodies/${string}`>, ): OpenApi.IOperation.IRequestBody | undefined => { if (TypeChecker.isReference(input)) { const found: OpenApiV3.IOperation.IRequestBody | undefined = doc.components?.requestBodies?.[input.$ref.split("/").pop() ?? ""]; if (found === undefined) return undefined; input = found; } return { ...input, content: input.content ? convertContent(doc.components ?? {})(input.content) : undefined, }; }; const convertResponse = (doc: OpenApiV3.IDocument) => ( input: | OpenApiV3.IOperation.IResponse | OpenApiV3.IJsonSchema.IReference<`#/components/responses/${string}`>, ): OpenApi.IOperation.IResponse | undefined => { if (TypeChecker.isReference(input)) { const found: OpenApiV3.IOperation.IResponse | undefined = doc.components?.responses?.[input.$ref.split("/").pop() ?? ""]; if (found === undefined) return undefined; input = found; } return { ...input, content: input.content ? convertContent(doc.components ?? {})(input.content) : undefined, headers: input.headers ? Object.fromEntries( Object.entries(input.headers) .filter(([_, v]) => v !== undefined) .map( ([key, value]) => [ key, (() => { if (TypeChecker.isReference(value) === false) return convertParameter(doc.components ?? {})({ ...value, in: "header", }); const found: | Omit<OpenApiV3.IOperation.IParameter, "in"> | undefined = value.$ref.startsWith( "#/components/headers/", ) ? doc.components?.headers?.[ value.$ref.split("/").pop() ?? "" ] : undefined; return found !== undefined ? convertParameter(doc.components ?? {})({ ...found, in: "header", }) : undefined!; })(), ] as const, ) .filter(([_, v]) => v !== undefined), ) : undefined, }; }; const convertContent = (components: OpenApiV3.IComponents) => ( record: Record<string, OpenApiV3.IOperation.IMediaType>, ): Record<string, OpenApi.IOperation.IMediaType> => Object.fromEntries( Object.entries(record) .filter(([_, v]) => v !== undefined) .map( ([key, value]) => [ key, { ...value, schema: value.schema ? convertSchema(components)(value.schema) : undefined, examples: value.examples ? Object.fromEntries( Object.entries(value.examples) .map(([key, value]) => [ key, TypeChecker.isReference(value) ? components.examples?.[ value.$ref.split("/").pop() ?? "" ] : value, ]) .filter(([_, v]) => v !== undefined), ) : undefined, }, ] as const, ), ); /* ----------------------------------------------------------- DEFINITIONS ----------------------------------------------------------- */ export const convertComponents = ( input: OpenApiV3.IComponents, ): OpenApi.IComponents => ({ schemas: Object.fromEntries( Object.entries(input.schemas ?? {}) .filter(([_, v]) => v !== undefined) .map(([key, value]) => [key, convertSchema(input)(value)]), ), securitySchemes: input.securitySchemes, }); export const convertSchema = (components: OpenApiV3.IComponents) => (input: OpenApiV3.IJsonSchema): OpenApi.IJsonSchema => { const nullable: { value: boolean; default?: null } = { value: false, default: undefined, }; const union: OpenApi.IJsonSchema[] = []; const attribute: OpenApi.IJsonSchema.__IAttribute = { title: input.title, description: input.description, ...Object.fromEntries( Object.entries(input).filter( ([key, value]) => key.startsWith("x-") && value !== undefined, ), ), example: input.example, examples: input.examples, }; const visit = (schema: OpenApiV3.IJsonSchema): void => { // NULLABLE PROPERTY if ( (schema as OpenApiV3.IJsonSchema.__ISignificant<any>).nullable === true ) { nullable.value ||= true; if ((schema as OpenApiV3.IJsonSchema.INumber).default === null) nullable.default = null; } if ( Array.isArray((schema as OpenApiV3.IJsonSchema.INumber).enum) && (schema as OpenApiV3.IJsonSchema.INumber).enum?.length && (schema as OpenApiV3.IJsonSchema.INumber).enum?.some( (e) => e === null, ) ) nullable.value ||= true; // UNION TYPE CASE if (TypeChecker.isAnyOf(schema)) schema.anyOf.forEach(visit); else if (TypeChecker.isOneOf(schema)) schema.oneOf.forEach(visit); else if (TypeChecker.isAllOf(schema)) if (schema.allOf.length === 1) visit(schema.allOf[0]); else union.push(convertAllOfSchema(components)(schema)); // ATOMIC TYPE CASE (CONSIDER ENUM VALUES) else if ( TypeChecker.isBoolean(schema) || TypeChecker.isInteger(schema) || TypeChecker.isNumber(schema) || TypeChecker.isString(schema) ) if ( schema.enum?.length && schema.enum.filter((e) => e !== null).length ) union.push( ...schema.enum .filter((v) => v !== null) .map((value) => ({ const: value })), ); else union.push({ ...schema, default: (schema.default ?? undefined) satisfies | boolean | number | string | undefined as any, ...{ enum: undefined }, }); // INSTANCE TYPE CASE else if (TypeChecker.isArray(schema)) union.push({ ...schema, items: convertSchema(components)(schema.items), }); else if (TypeChecker.isObject(schema)) union.push({ ...schema, ...{ properties: schema.properties ? Object.fromEntries( Object.entries(schema.properties) .filter(([_, v]) => v !== undefined) .map(([key, value]) => [ key, convertSchema(components)(value), ]), ) : {}, additionalProperties: schema.additionalProperties ? typeof schema.additionalProperties === "object" && schema.additionalProperties !== null ? convertSchema(components)(schema.additionalProperties) : schema.additionalProperties : undefined, required: schema.required ?? [], }, }); else if (TypeChecker.isReference(schema)) union.push(schema); else union.push(schema); }; visit(input); if ( nullable.value === true && !union.some((e) => (e as OpenApi.IJsonSchema.INull).type === "null") ) union.push({ type: "null", default: nullable.default, }); if ( union.length === 2 && union.filter((x) => OpenApiTypeChecker.isNull(x)).length === 1 ) { const type: OpenApi.IJsonSchema = union.filter( (x) => OpenApiTypeChecker.isNull(x) === false, )[0]; for (const key of [ "title", "description", "deprecated", "example", "examples", ] as const) if (type[key] !== undefined) delete type[key]; } return { ...(union.length === 0 ? { type: undefined } : union.length === 1 ? { ...union[0] } : { oneOf: union.map((u) => ({ ...u, nullable: undefined })) }), ...attribute, ...{ nullable: undefined }, }; }; const convertAllOfSchema = (components: OpenApiV3.IComponents) => (input: OpenApiV3.IJsonSchema.IAllOf): OpenApi.IJsonSchema => { const objects: Array<OpenApiV3.IJsonSchema.IObject | null> = input.allOf.map((schema) => retrieveObject(components)(schema)); if (objects.some((obj) => obj === null)) return { type: undefined, ...{ allOf: undefined, }, }; return { ...input, type: "object", properties: Object.fromEntries( objects .map((o) => Object.entries(o?.properties ?? {})) .flat() .map( ([key, value]) => [key, convertSchema(components)(value)] as const, ), ), ...{ allOf: undefined, required: [...new Set(objects.map((o) => o?.required ?? []).flat())], }, }; }; const retrieveObject = (components: OpenApiV3.IComponents) => ( input: OpenApiV3.IJsonSchema, visited: Set<OpenApiV3.IJsonSchema> = new Set(), ): OpenApiV3.IJsonSchema.IObject | null => { if (TypeChecker.isObject(input)) return input.properties !== undefined && !input.additionalProperties ? input : null; else if (visited.has(input)) return null; else visited.add(input); if (TypeChecker.isReference(input)) return retrieveObject(components)( components.schemas?.[input.$ref.split("/").pop() ?? ""] ?? {}, visited, ); return null; }; export namespace TypeChecker { export const isBoolean = ( schema: OpenApiV3.IJsonSchema, ): schema is OpenApiV3.IJsonSchema.IBoolean => (schema as OpenApiV3.IJsonSchema.IBoolean).type === "boolean"; export const isInteger = ( schema: OpenApiV3.IJsonSchema, ): schema is OpenApiV3.IJsonSchema.IInteger => (schema as OpenApiV3.IJsonSchema.IInteger).type === "integer"; export const isNumber = ( schema: OpenApiV3.IJsonSchema, ): schema is OpenApiV3.IJsonSchema.INumber => (schema as OpenApiV3.IJsonSchema.INumber).type === "number"; export const isString = ( schema: OpenApiV3.IJsonSchema, ): schema is OpenApiV3.IJsonSchema.IString => (schema as OpenApiV3.IJsonSchema.IString).type === "string"; export const isArray = ( schema: OpenApiV3.IJsonSchema, ): schema is OpenApiV3.IJsonSchema.IArray => (schema as OpenApiV3.IJsonSchema.IArray).type === "array"; export const isObject = ( schema: OpenApiV3.IJsonSchema, ): schema is OpenApiV3.IJsonSchema.IObject => (schema as OpenApiV3.IJsonSchema.IObject).type === "object"; export const isReference = ( schema: OpenApiV3.IJsonSchema, ): schema is OpenApiV3.IJsonSchema.IReference => (schema as OpenApiV3.IJsonSchema.IReference).$ref !== undefined; export const isAllOf = ( schema: OpenApiV3.IJsonSchema, ): schema is OpenApiV3.IJsonSchema.IAllOf => (schema as OpenApiV3.IJsonSchema.IAllOf).allOf !== undefined; export const isAnyOf = ( schema: OpenApiV3.IJsonSchema, ): schema is OpenApiV3.IJsonSchema.IAnyOf => (schema as OpenApiV3.IJsonSchema.IAnyOf).anyOf !== undefined; export const isOneOf = ( schema: OpenApiV3.IJsonSchema, ): schema is OpenApiV3.IJsonSchema.IOneOf => (schema as OpenApiV3.IJsonSchema.IOneOf).oneOf !== undefined; export const isNullOnly = ( schema: OpenApiV3.IJsonSchema, ): schema is OpenApiV3.IJsonSchema.INullOnly => (schema as OpenApiV3.IJsonSchema.INullOnly).type === "null"; } }