UNPKG

openapi-metadata

Version:

Auto-Generate OpenAPI specifications from Typescript decorators

149 lines (121 loc) 4 kB
import type { OpenAPIV3 } from "openapi-types"; import type { Context } from "../context.js"; import type { TypeLoaderFn, TypeOptions } from "../types.js"; import type { SetRequired } from "type-fest"; import { getEnumType, getEnumValues } from "../utils/enum.js"; import { PropertyMetadataStorage } from "../metadata/property.js"; import { getSchemaPath } from "../utils/schema.js"; import { isThunk } from "../utils/metadata.js"; /** * Type loader to load primitive types. */ export const PrimitiveTypeLoader: TypeLoaderFn = async (_context, value) => { if (typeof value === "string") { return { type: value }; } // biome-ignore lint/suspicious/noDoubleEquals: strict comparaison might fail to catch it if (value == String) { return { type: "string" }; } // biome-ignore lint/suspicious/noDoubleEquals: strict comparaison might fail to catch it if (value == Boolean) { return { type: "boolean" }; } // biome-ignore lint/suspicious/noDoubleEquals: strict comparaison might fail to catch it if (value == Number) { return { type: "number" }; } }; /** * Type loader to load array types. */ export const ArrayTypeLoader: TypeLoaderFn = async (context, value) => { if (!Array.isArray(value)) { return; } if (value.length <= 0) { context.logger.warn("You tried to specify an array type without any item"); return; } if (value.length > 1) { context.logger.warn( "You tried to specify an array type with multiple items. Please use the 'enum' option if you want to specify an enum.", ); return; } const itemsSchema = await loadType(context, { type: value[0] }); // TODO: Better warn stack trace if (!itemsSchema) { context.logger.warn("You tried to specify an array type with an item that resolves to undefined."); return; } return { type: "array", items: itemsSchema, }; }; /** * Type loader to load classes as SchemaObject. */ export const ClassTypeLoader: TypeLoaderFn = async (context, value) => { if (typeof value !== "function" || !value.prototype) { return; } const model = value.name; if (context.schemas[model]) { return { $ref: getSchemaPath(model) }; } const schema: SetRequired<OpenAPIV3.SchemaObject, "properties" | "required"> = { type: "object", properties: {}, required: [], }; const properties = PropertyMetadataStorage.getMetadata(value.prototype); if (!properties) { context.logger.warn(`You tried to use '${model}' as a type but it does not contain any ApiProperty.`); } context.schemas[model] = schema; for (const [key, property] of Object.entries(properties)) { const { required, type, name, enum: e, schema: s, ...metadata } = property as any; schema.properties[key] = { ...(await loadType(context, property)), ...metadata, }; if (property.required) { schema.required.push(key); } } return { $ref: getSchemaPath(model) }; }; /** * Transforms a type option into a SchemaObject or ReferenceObject. */ export async function loadType( context: Context, options: TypeOptions, ): Promise<OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined> { if (options.schema) { return options.schema; } if (options.enum) { const enumValues = getEnumValues(options.enum); const enumType = getEnumType(enumValues); return { type: enumType, enum: enumValues, }; } if (!options.type) { context.logger.warn("Failed to infer type from property"); return; } const thunk = isThunk(options.type); const value = thunk ? (options.type as Function)(context) : options.type; for (const loader of [PrimitiveTypeLoader, ArrayTypeLoader, ...context.typeLoaders, ClassTypeLoader]) { const result = await loader(context, value, options.type); if (result) { return result; } } context.logger.warn(`You tried to use '${options.type.toString()}' as a type but no loader supports it ${thunk}`); }