UNPKG

@samchon/openapi

Version:

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

300 lines (284 loc) 9.17 kB
import { OpenApi } from "../../OpenApi"; import { MapUtil } from "../MapUtil"; import { OpenApiTypeChecker } from "../OpenApiTypeChecker"; import { IOpenApiValidatorContext } from "./IOpenApiValidatorContext"; import { OpenApiStationValidator } from "./OpenApiStationValidator"; export namespace OpenApiOneOfValidator { export const validate = ( ctx: IOpenApiValidatorContext<OpenApi.IJsonSchema.IOneOf>, ): boolean => { const discriminator: IDiscriminator = getDiscriminator(ctx); for (const item of discriminator.branches) if (item.predicator(ctx.value)) return OpenApiStationValidator.validate({ ...ctx, schema: item.schema, }); return discriminator.branches.length === 0 ? discriminator.remainders .map((schema) => OpenApiStationValidator.validate({ ...ctx, schema, exceptionable: false, }), ) .some((v) => v) || ctx.report(ctx) : validate({ ...ctx, schema: { oneOf: discriminator.remainders, }, }); }; const getDiscriminator = ( ctx: IOpenApiValidatorContext<OpenApi.IJsonSchema.IOneOf>, ): IDiscriminator => { const resolvedList: IFlatSchema[] = ctx.schema.oneOf.map((schema) => getFlattened({ components: ctx.components, schema, visited: new Set(), }), ); // FIND ANY TYPE const anything: IFlatSchema | undefined = resolvedList.find((resolved) => OpenApiTypeChecker.isUnknown(resolved.escaped), ); if (anything) return { branches: [], remainders: [anything.schema], }; // CHECK NULLABLES const nullables: IFlatSchema<OpenApi.IJsonSchema.INull>[] = resolvedList.filter( (resolved): resolved is IFlatSchema<OpenApi.IJsonSchema.INull> => OpenApiTypeChecker.isNull(resolved.schema), ); const significant: IFlatSchema<OpenApi.IJsonSchema>[] = resolvedList.filter( (resolved) => false === OpenApiTypeChecker.isNull(resolved.escaped), ); if (significant.length === 1) return { branches: [ { schema: significant[0].schema, predicator: (value) => value !== null, }, ], remainders: nullables.map((nullable) => nullable.schema), }; // DISCRIMINATIONS const tuples = significant.filter((flat) => OpenApiTypeChecker.isTuple(flat.escaped), ); const arrays = significant.filter( (flat): flat is IFlatSchema<OpenApi.IJsonSchema.IArray> => OpenApiTypeChecker.isArray(flat.escaped), ); const branches: IDiscriminatorBranch[] = [ ...(tuples.length === 0 && arrays.length !== 0 ? discriminateArrays( ctx, significant.filter( (flat): flat is IFlatSchema<OpenApi.IJsonSchema.IArray> => OpenApiTypeChecker.isArray(flat.schema), ), ) : []), ...discriminateObjects( ctx, significant.filter( (flat): flat is IFlatSchema<OpenApi.IJsonSchema.IObject> => OpenApiTypeChecker.isObject(flat.escaped), ), tuples.length + arrays.length === 0, ), ]; return { branches, remainders: ctx.schema.oneOf.filter( (x) => branches.some((y) => y.schema === x) === false, ), }; }; const discriminateArrays = ( ctx: IOpenApiValidatorContext<OpenApi.IJsonSchema.IOneOf>, arraySchemas: IFlatSchema<OpenApi.IJsonSchema.IArray>[], ): IDiscriminatorBranch[] => { if (arraySchemas.length === 1) return [ { schema: arraySchemas[0].schema, predicator: (value) => Array.isArray(value), }, ]; return arraySchemas .filter((flat, i, array) => array.every( (item, j) => i === j || !OpenApiTypeChecker.covers({ components: ctx.components, x: item.escaped.items, y: flat.escaped.items, }), ), ) .map( (flat) => ({ schema: flat.schema, predicator: (value) => Array.isArray(value) && (value.length === 0 || OpenApiStationValidator.validate({ ...ctx, schema: (flat.escaped as OpenApi.IJsonSchema.IArray).items, value: value[0]!, path: `${ctx.path}[0]`, exceptionable: false, })), }) satisfies IDiscriminatorBranch, ); }; const discriminateObjects = ( ctx: IOpenApiValidatorContext<OpenApi.IJsonSchema.IOneOf>, objectSchemas: IFlatSchema<OpenApi.IJsonSchema.IObject>[], noArray: boolean, ): IDiscriminatorBranch[] => { if (objectSchemas.length === 1) return [ { schema: objectSchemas[0].schema, predicator: noArray ? (value) => typeof value === "object" && value !== null : (value) => typeof value === "object" && value !== null && Array.isArray(value) === false, }, ]; // KEEP ONLY REQUIRED PROPERTIES objectSchemas = objectSchemas .filter( (flat) => flat.escaped.properties !== undefined && flat.escaped.required !== undefined, ) .map( (flat) => ({ ...flat, escaped: { ...flat.escaped, properties: Object.fromEntries( Object.entries(flat.escaped.properties ?? {}).map( ([key, value]) => [ key, getFlattened({ components: ctx.components, schema: value, visited: new Set(), }).escaped, ], ), ), }, }) satisfies IFlatSchema<OpenApi.IJsonSchema.IObject>, ); // PROPERTY MATRIX const matrix: Map<string, Array<OpenApi.IJsonSchema | null>> = new Map(); objectSchemas.forEach((obj, i) => { for (const [key, value] of Object.entries(obj.escaped.properties ?? {})) { if (!!obj.escaped.required?.includes(key) === false) continue; MapUtil.take(matrix)(key)(() => new Array(objectSchemas.length).fill(null), )[i] = value; } }); // THE BRANCHES return objectSchemas .map((obj, i) => { const candidates: string[] = []; for (const [key, value] of Object.entries( obj.escaped.properties ?? {}, )) { if (!!obj.escaped.required?.includes(key) === false) continue; const neighbors: OpenApi.IJsonSchema[] = matrix .get(key)! .filter((_oppo, j) => i !== j) .filter((oppo) => oppo !== null); const unique: boolean = OpenApiTypeChecker.isConstant(value) ? neighbors.every( (oppo) => OpenApiTypeChecker.isConstant(oppo) && value.const !== oppo.const, ) : neighbors.length === 0; if (unique) candidates.push(key); } if (candidates.length === 0) return null; const top: string = candidates.find((key) => OpenApiTypeChecker.isConstant(obj.escaped.properties![key]), ) ?? candidates[0]; const target: OpenApi.IJsonSchema = obj.escaped.properties![top]; return { schema: obj.schema, predicator: OpenApiTypeChecker.isConstant(target) ? (value) => typeof value === "object" && value !== null && (value as any)[top] === target.const : (value) => typeof value === "object" && value !== null && (value as any)[top] !== undefined, } satisfies IDiscriminatorBranch; }) .filter((b) => b !== null); }; } const getFlattened = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema; visited: Set<string>; }): IFlatSchema => { if (OpenApiTypeChecker.isReference(props.schema)) { const key: string = props.schema.$ref.split("/").pop() ?? ""; if (props.visited.has(key)) return { schema: props.schema, escaped: {}, }; props.visited.add(key); return { ...getFlattened({ components: props.components, schema: props.components.schemas?.[key] ?? {}, visited: props.visited, }), schema: props.schema, }; } return { schema: props.schema, escaped: props.schema, }; }; interface IDiscriminator { branches: IDiscriminatorBranch[]; remainders: OpenApi.IJsonSchema[]; } interface IDiscriminatorBranch { schema: OpenApi.IJsonSchema; predicator: (value: unknown) => boolean; } interface IFlatSchema< Schema extends OpenApi.IJsonSchema = OpenApi.IJsonSchema, > { schema: OpenApi.IJsonSchema; escaped: Schema; }