UNPKG

typia

Version:

Superfast runtime validators with only one line

550 lines (508 loc) • 17.5 kB
import ts from "typescript"; import { IdentifierFactory } from "../factories/IdentifierFactory"; import { MetadataCollection } from "../factories/MetadataCollection"; import { StatementFactory } from "../factories/StatementFactory"; import { TypeFactory } from "../factories/TypeFactory"; import { ValueFactory } from "../factories/ValueFactory"; import { Metadata } from "../schemas/metadata/Metadata"; import { MetadataArray } from "../schemas/metadata/MetadataArray"; import { MetadataObject } from "../schemas/metadata/MetadataObject"; import { IProject } from "../transformers/IProject"; import { CheckerProgrammer } from "./CheckerProgrammer"; import { FunctionImporter } from "./helpers/FunctionImporter"; import { IExpressionEntry } from "./helpers/IExpressionEntry"; import { UnionExplorer } from "./helpers/UnionExplorer"; import { feature_object_entries } from "./internal/feature_object_entries"; export namespace FeatureProgrammer { /* ----------------------------------------------------------- PARAMETERS ----------------------------------------------------------- */ export interface IConfig<Output extends ts.ConciseBody = ts.ConciseBody> { types: IConfig.ITypes; /** * Prefix name of internal functions for specific types. */ prefix: string; /** * Whether to archive access path or not. */ path: boolean; /** * Whether to trace exception or not. */ trace: boolean; addition?: undefined | ((collection: MetadataCollection) => ts.Statement[]); /** * Initializer of metadata. */ initializer: ( project: IProject, ) => ( importer: FunctionImporter, ) => (type: ts.Type) => [MetadataCollection, Metadata]; /** * Decoder, station of every types. */ decoder: () => Decoder<Metadata, Output>; /** * Object configurator. */ objector: IConfig.IObjector<Output>; /** * Generator of functions for object types. */ generator: IConfig.IGenerator; } export namespace IConfig { export interface ITypes { input: (type: ts.Type, name?: undefined | string) => ts.TypeNode; output: (type: ts.Type, name?: undefined | string) => ts.TypeNode; } export interface IObjector<Output extends ts.ConciseBody = ts.ConciseBody> { /** * Type checker when union object type comes. */ checker: () => Decoder<Metadata, ts.Expression>; /** * Decoder, function call expression generator of specific typed objects. */ decoder: () => Decoder<MetadataObject, ts.Expression>; /** * Joiner of expressions from properties. */ joiner( input: ts.Expression, entries: IExpressionEntry<Output>[], parent: MetadataObject, ): ts.ConciseBody; /** * Union type specificator. * * Expression of an algorithm specifying object type and calling * the `decoder` function of the specified object type. */ unionizer: Decoder<MetadataObject[], ts.Expression>; /** * Handler of union type specification failure. * * @param value Expression of input parameter * @param expected Expected type name * @param explore Exploration info * @returns Statement of failure */ failure( value: ts.Expression, expected: string, explore?: undefined | IExplore, ): ts.Statement; /** * Transformer of type checking expression by discrimination. * * When an object type has been specified by a discrimination without full * iteration, the `unionizer` will decode the object instance after * the last type checking. * * In such circumtance, you can transform the last type checking function. * * @param exp Current expression about type checking * @returns Transformed expression * @deprecated */ is?: undefined | ((exp: ts.Expression) => ts.Expression); /** * Transformer of non-undefined type checking by discrimination. * * When specifying an union type of objects, `typia` tries to find * descrimination way just by checking only one property type. * If succeeded to find the discrimination way, `typia` will check the target * property type and in the checking, non-undefined type checking would be * done. * * In such process, you can transform the non-undefined type checking. * * @param exp * @returns Transformed expression * @deprecated */ required?: undefined | ((exp: ts.Expression) => ts.Expression); /** * Conditon wrapper when unable to specify any object type. * * When failed to specify an object type through discrimination, full * iteration type checking would be happend. In such circumstance, you * can wrap the condition with additional function. * * @param condition Current condition * @returns A function wrapped current condition */ full?: | undefined | (( condition: ts.Expression, ) => ( input: ts.Expression, expected: string, explore: IExplore, ) => ts.Expression); /** * Return type. */ type?: undefined | ts.TypeNode; } export interface IGenerator { objects?: | undefined | (() => (col: MetadataCollection) => ts.VariableStatement[]); unions?: | undefined | (() => (col: MetadataCollection) => ts.VariableStatement[]); arrays(): (col: MetadataCollection) => ts.VariableStatement[]; tuples(): (col: MetadataCollection) => ts.VariableStatement[]; } } export interface IExplore { tracable: boolean; source: "top" | "function"; from: "top" | "array" | "object"; postfix: string; start?: undefined | number; } export interface Decoder<T, Output extends ts.ConciseBody = ts.ConciseBody> { (input: ts.Expression, target: T, explore: IExplore): Output; } /* ----------------------------------------------------------- GENERATORS ----------------------------------------------------------- */ export interface IComposed { body: ts.ConciseBody; parameters: ts.ParameterDeclaration[]; functions: Record<string, ts.VariableStatement>; statements: ts.Statement[]; response: ts.TypeNode; } export interface IDecomposed { functions: Record<string, ts.VariableStatement>; statements: ts.Statement[]; arrow: ts.ArrowFunction; } export const compose = (props: { project: IProject; config: IConfig; importer: FunctionImporter; type: ts.Type; name: string | undefined; }): IComposed => { const [collection, meta] = props.config.initializer(props.project)( props.importer, )(props.type); return { body: props.config.decoder()(ValueFactory.INPUT(), meta, { tracable: props.config.path || props.config.trace, source: "top", from: "top", postfix: '""', }), statements: props.config.addition ? props.config.addition(collection) : [], functions: { ...Object.fromEntries( ( props.config.generator.objects?.() ?? write_object_functions(props.config)(props.importer) )(collection).map((v, i) => [`${props.config.prefix}o${i}`, v]), ), ...Object.fromEntries( ( props.config.generator.unions?.() ?? write_union_functions(props.config) )(collection).map((v, i) => [`${props.config.prefix}u${i}`, v]), ), ...Object.fromEntries( props.config.generator .arrays()(collection) .map((v, i) => [`${props.config.prefix}a${i}`, v]), ), ...Object.fromEntries( props.config.generator .tuples()(collection) .map((v, i) => [`${props.config.prefix}t${i}`, v]), ), }, parameters: parameterDeclarations(props.config)( props.config.types.input(props.type, props.name), )(ValueFactory.INPUT()), response: props.config.types.output(props.type, props.name), }; }; export const writeDecomposed = (props: { modulo: ts.LeftHandSideExpression; importer: FunctionImporter; result: IDecomposed; }): ts.CallExpression => ts.factory.createCallExpression( ts.factory.createArrowFunction( undefined, undefined, [], undefined, undefined, ts.factory.createBlock([ ...props.importer.declare(props.modulo), ...Object.entries(props.result.functions) .filter(([k]) => props.importer.hasLocal(k)) .map(([_k, v]) => v), ...props.result.statements, ts.factory.createReturnStatement(props.result.arrow), ]), ), undefined, undefined, ); export const write = (project: IProject) => (config: IConfig) => (importer: FunctionImporter) => (type: ts.Type, name?: string): ts.ArrowFunction => { const [collection, meta] = config.initializer(project)(importer)(type); // ITERATE OVER ALL METADATA const output: ts.ConciseBody = config.decoder()( ValueFactory.INPUT(), meta, { tracable: config.path || config.trace, source: "top", from: "top", postfix: '""', }, ); // RETURNS THE OPTIMAL ARROW FUNCTION const functions = { objects: ( config.generator.objects?.() ?? write_object_functions(config)(importer) )(collection), unions: (config.generator.unions?.() ?? write_union_functions(config))( collection, ), arrays: config.generator.arrays()(collection), tuples: config.generator.tuples()(collection), }; const added: ts.Statement[] = (config.addition ?? (() => []))(collection); return ts.factory.createArrowFunction( undefined, undefined, parameterDeclarations(config)(config.types.input(type, name))( ValueFactory.INPUT(), ), config.types.output(type, name), undefined, ts.factory.createBlock( [ ...added, ...functions.objects.filter((_, i) => importer.hasLocal(`${config.prefix}o${i}`), ), ...functions.unions.filter((_, i) => importer.hasLocal(`${config.prefix}u${i}`), ), ...functions.arrays.filter((_, i) => importer.hasLocal(`${config.prefix}a${i}`), ), ...functions.tuples.filter((_, i) => importer.hasLocal(`${config.prefix}t${i}`), ), ...(ts.isBlock(output) ? output.statements : [ts.factory.createReturnStatement(output)]), ], true, ), ); }; export const write_object_functions = (config: IConfig) => (importer: FunctionImporter) => (collection: MetadataCollection) => collection .objects() .map((obj) => StatementFactory.constant( `${config.prefix}o${obj.index}`, ts.factory.createArrowFunction( undefined, undefined, parameterDeclarations(config)(TypeFactory.keyword("any"))( ValueFactory.INPUT(), ), config.objector.type ?? TypeFactory.keyword("any"), undefined, config.objector.joiner( ts.factory.createIdentifier("input"), feature_object_entries(config)(importer)(obj)( ts.factory.createIdentifier("input"), ), obj, ), ), ), ); export const write_union_functions = (config: IConfig) => (collection: MetadataCollection) => collection .unions() .map((union, i) => StatementFactory.constant( `${config.prefix}u${i}`, write_union(config)(union), ), ); const write_union = (config: IConfig) => { const explorer = UnionExplorer.object(config); const input = ValueFactory.INPUT(); return (meta: MetadataObject[]) => ts.factory.createArrowFunction( undefined, undefined, parameterDeclarations(config)(TypeFactory.keyword("any"))( ValueFactory.INPUT(), ), TypeFactory.keyword("any"), undefined, explorer(input, meta, { tracable: config.path || config.trace, source: "function", from: "object", postfix: "", }), ); }; /* ----------------------------------------------------------- DECODERS ----------------------------------------------------------- */ export const decode_array = (config: Pick<IConfig, "trace" | "path" | "decoder" | "prefix">) => (importer: FunctionImporter) => ( combiner: ( input: ts.Expression, arrow: ts.ArrowFunction, ) => ts.Expression, ) => { const rand: string = importer.increment().toString(); const tail = config.path || config.trace ? [ IdentifierFactory.parameter( "_index" + rand, TypeFactory.keyword("number"), ), ] : []; return ( input: ts.Expression, array: MetadataArray, explore: IExplore, ) => { const arrow: ts.ArrowFunction = ts.factory.createArrowFunction( undefined, undefined, [ IdentifierFactory.parameter("elem", TypeFactory.keyword("any")), ...tail, ], undefined, undefined, config.decoder()(ValueFactory.INPUT("elem"), array.type.value, { tracable: explore.tracable, source: explore.source, from: "array", postfix: index(explore.start ?? null)(explore.postfix)(rand), }), ); return combiner(input, arrow); }; }; export const decode_object = (config: Pick<IConfig, "trace" | "path" | "prefix">) => (importer: FunctionImporter) => (input: ts.Expression, obj: MetadataObject, explore: IExplore) => ts.factory.createCallExpression( ts.factory.createIdentifier( importer.useLocal(`${config.prefix}o${obj.index}`), ), undefined, argumentsArray(config)(explore)(input), ); /* ----------------------------------------------------------- UTILITIES FOR INTERNAL FUNCTIONS ----------------------------------------------------------- */ export const index = (start: number | null) => (prev: string) => (rand: string) => { const tail: string = start !== null ? `"[" + (${start} + _index${rand}) + "]"` : `"[" + _index${rand} + "]"`; if (prev === "") return tail; else if (prev[prev.length - 1] === `"`) return prev.substring(0, prev.length - 1) + tail.substring(1); return prev + ` + ${tail}`; }; export const argumentsArray = (config: Pick<IConfig, "path" | "trace">) => (explore: FeatureProgrammer.IExplore) => { const tail: ts.Expression[] = config.path === false && config.trace === false ? [] : config.path === true && config.trace === true ? [ ts.factory.createIdentifier( explore.postfix ? `_path + ${explore.postfix}` : "_path", ), explore.source === "function" ? ts.factory.createIdentifier( `${explore.tracable} && _exceptionable`, ) : explore.tracable ? ts.factory.createTrue() : ts.factory.createFalse(), ] : config.path === true ? [ ts.factory.createIdentifier( explore.postfix ? `_path + ${explore.postfix}` : "_path", ), ] : [ explore.source === "function" ? ts.factory.createIdentifier( `${explore.tracable} && _exceptionable`, ) : explore.tracable ? ts.factory.createTrue() : ts.factory.createFalse(), ]; return (input: ts.Expression) => [input, ...tail]; }; export const parameterDeclarations = (props: Pick<CheckerProgrammer.IConfig, "path" | "trace">) => (type: ts.TypeNode) => { const tail: ts.ParameterDeclaration[] = []; if (props.path) tail.push( IdentifierFactory.parameter("_path", TypeFactory.keyword("string")), ); if (props.trace) tail.push( IdentifierFactory.parameter( "_exceptionable", TypeFactory.keyword("boolean"), ts.factory.createTrue(), ), ); return (input: ts.Identifier): ts.ParameterDeclaration[] => [ IdentifierFactory.parameter(input, type), ...tail, ]; }; }