UNPKG

typia

Version:

Superfast runtime validators with only one line

405 lines (379 loc) 10.4 kB
import ts from "typescript"; import { Metadata } from "../schemas/metadata/Metadata"; import { MetadataAliasType } from "../schemas/metadata/MetadataAliasType"; import { MetadataArrayType } from "../schemas/metadata/MetadataArrayType"; import { MetadataConstant } from "../schemas/metadata/MetadataConstant"; import { MetadataFunction } from "../schemas/metadata/MetadataFunction"; import { MetadataObject } from "../schemas/metadata/MetadataObject"; import { MetadataObjectType } from "../schemas/metadata/MetadataObjectType"; import { MetadataTupleType } from "../schemas/metadata/MetadataTupleType"; import { explore_metadata } from "./internal/metadata/explore_metadata"; import { iterate_metadata_collection } from "./internal/metadata/iterate_metadata_collection"; import { iterate_metadata_sort } from "./internal/metadata/iterate_metadata_sort"; import { ValidationPipe } from "../typings/ValidationPipe"; import { ExpressionFactory } from "./ExpressionFactory"; import { MetadataCollection } from "./MetadataCollection"; export namespace MetadataFactory { export type Validator = (meta: Metadata, explore: IExplore) => string[]; export interface IProps { checker: ts.TypeChecker; transformer: ts.TransformationContext | undefined; options: IOptions; collection: MetadataCollection; type: ts.Type | null; } export interface IOptions { escape: boolean; constant: boolean; absorb: boolean; functional?: boolean; validate?: Validator; onError?: (node: ts.Node | undefined, message: string) => void; } export interface IExplore { top: boolean; object: MetadataObjectType | null; property: string | object | null; nested: null | MetadataAliasType | MetadataArrayType | MetadataTupleType; parameter: string | null; output: boolean; escaped: boolean; aliased: boolean; } export interface IError { name: string; explore: IExplore; messages: string[]; } export const analyze = (props: IProps): ValidationPipe<Metadata, IError> => { const errors: IError[] = []; const metadata: Metadata = explore_metadata({ ...props, errors, explore: { top: true, object: null, property: null, parameter: null, nested: null, aliased: false, escaped: false, output: false, }, intersected: false, }); iterate_metadata_collection({ errors, collection: props.collection, }); iterate_metadata_sort({ collection: props.collection, metadata: metadata, }); if (props.options.validate) errors.push( ...validate({ transformer: props.transformer, options: props.options, functor: props.options.validate, metadata, }), ); return errors.length ? { success: false, errors, } : { success: true, data: metadata, }; }; /** * @internal */ export const soleLiteral = (value: string): Metadata => { const meta: Metadata = Metadata.initialize(); meta.constants.push( MetadataConstant.from({ values: [ { value, tags: [], }, ], type: "string", }), ); return meta; }; export const validate = (props: { transformer?: ts.TransformationContext; options: IOptions; functor: Validator; metadata: Metadata; }): IError[] => { const visitor: IValidationVisitor = { functor: props.functor, errors: [], objects: new Set(), arrays: new Set(), tuples: new Set(), aliases: new Set(), functions: new Set(), }; validateMeta({ ...props, visitor, explore: { object: null, property: null, parameter: null, nested: null, top: true, aliased: false, escaped: false, output: false, }, }); return visitor.errors; }; const validateMeta = (props: { options: IOptions; visitor: IValidationVisitor; metadata: Metadata; explore: IExplore; }) => { const result: string[] = []; for (const atomic of props.metadata.atomics) for (const row of atomic.tags) for (const tag of row.filter( (t) => t.validate !== undefined && t.predicate === undefined, )) try { tag.predicate = ExpressionFactory.transpile({ script: tag.validate!, }); } catch { result.push( `Unable to transpile type tag script: ${JSON.stringify( tag.validate, )}`, ); tag.predicate = () => ts.factory.createTrue(); } result.push(...props.visitor.functor(props.metadata, props.explore)); if (result.length) props.visitor.errors.push({ name: props.metadata.getName(), explore: { ...props.explore }, messages: [...new Set(result)], }); for (const alias of props.metadata.aliases) validateAlias({ ...props, alias: alias.type, }); for (const array of props.metadata.arrays) validateArray({ ...props, array: array.type, }); for (const tuple of props.metadata.tuples) validateTuple({ ...props, tuple: tuple.type, }); for (const object of props.metadata.objects) validateObject({ ...props, object: object.type, }); for (const func of props.metadata.functions) validateFunction({ ...props, function: func, }); for (const set of props.metadata.sets) validateMeta({ ...props, metadata: set.value, }); for (const map of props.metadata.maps) { validateMeta({ ...props, metadata: map.key, }); validateMeta({ ...props, metadata: map.value, }); } if (props.options.escape === true && props.metadata.escaped !== null) validateMeta({ ...props, metadata: props.metadata.escaped.returns, explore: { ...props.explore, escaped: true, }, }); }; const validateAlias = (props: { transformer?: ts.TransformationContext; options: IOptions; visitor: IValidationVisitor; alias: MetadataAliasType; explore: IExplore; }) => { if (props.visitor.aliases.has(props.alias)) return; props.visitor.aliases.add(props.alias); validateMeta({ ...props, metadata: props.alias.value, explore: { ...props.explore, nested: props.alias, aliased: true, }, }); }; const validateArray = (props: { transformer?: ts.TransformationContext; options: IOptions; visitor: IValidationVisitor; array: MetadataArrayType; explore: IExplore; }) => { if (props.visitor.arrays.has(props.array)) return; props.visitor.arrays.add(props.array); validateMeta({ ...props, metadata: props.array.value, explore: { ...props.explore, nested: props.array, top: false, }, }); }; const validateTuple = (props: { transformer?: ts.TransformationContext; options: IOptions; visitor: IValidationVisitor; tuple: MetadataTupleType; explore: IExplore; }) => { if (props.visitor.tuples.has(props.tuple)) return; props.visitor.tuples.add(props.tuple); for (const elem of props.tuple.elements) validateMeta({ ...props, metadata: elem, explore: { ...props.explore, nested: props.tuple, top: false, }, }); }; const validateObject = (props: { transformer?: ts.TransformationContext; options: IOptions; visitor: IValidationVisitor; object: MetadataObjectType; }) => { if (props.visitor.objects.has(props.object)) return; props.visitor.objects.add(props.object); if (props.options.validate) { const explore: IExplore = { object: props.object, top: false, property: null, parameter: null, nested: null, aliased: false, escaped: false, output: false, }; const errors: string[] = props.options.validate( Metadata.create({ ...Metadata.initialize(), objects: [ MetadataObject.create({ type: props.object, tags: [], }), ], }), explore, ); if (errors.length) props.visitor.errors.push({ name: props.object.name, explore, messages: [...new Set(errors)], }); } for (const property of props.object.properties) validateMeta({ ...props, metadata: property.value, explore: { object: props.object, property: property.key.isSoleLiteral() ? property.key.getSoleLiteral()! : {}, parameter: null, nested: null, top: false, aliased: false, escaped: false, output: false, }, }); }; const validateFunction = (props: { transformer?: ts.TransformationContext; options: IOptions; visitor: IValidationVisitor; function: MetadataFunction; explore: IExplore; }) => { if (props.visitor.functions.has(props.function)) return; props.visitor.functions.add(props.function); for (const param of props.function.parameters) validateMeta({ ...props, metadata: param.type, explore: { ...props.explore, parameter: param.name, nested: null, top: false, output: false, }, }); if (props.function.output) validateMeta({ ...props, metadata: props.function.output, explore: { ...props.explore, parameter: null, nested: null, top: false, output: true, }, }); }; interface IValidationVisitor { functor: Validator; errors: IError[]; objects: Set<MetadataObjectType>; arrays: Set<MetadataArrayType>; tuples: Set<MetadataTupleType>; aliases: Set<MetadataAliasType>; functions: Set<MetadataFunction>; } }