UNPKG

typia

Version:

Superfast runtime validators with only one line

910 lines (858 loc) • 28.6 kB
import ts from "typescript"; import { ExpressionFactory } from "../../factories/ExpressionFactory"; import { IdentifierFactory } from "../../factories/IdentifierFactory"; import { JsonMetadataFactory } from "../../factories/JsonMetadataFactory"; 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 { MetadataAtomic } from "../../schemas/metadata/MetadataAtomic"; import { MetadataTuple } from "../../schemas/metadata/MetadataTuple"; import { MetadataTupleType } from "../../schemas/metadata/MetadataTupleType"; import { IProject } from "../../transformers/IProject"; import { Atomic } from "../../typings/Atomic"; import { ArrayUtil } from "../../utils/ArrayUtil"; import { FeatureProgrammer } from "../FeatureProgrammer"; import { IsProgrammer } from "../IsProgrammer"; import { AtomicPredicator } from "../helpers/AtomicPredicator"; import { FunctionImporter } from "../helpers/FunctionImporter"; import { OptionPredicator } from "../helpers/OptionPredicator"; import { StringifyJoiner } from "../helpers/StringifyJoinder"; import { StringifyPredicator } from "../helpers/StringifyPredicator"; import { UnionExplorer } from "../helpers/UnionExplorer"; import { check_native } from "../internal/check_native"; import { decode_union_object } from "../internal/decode_union_object"; import { postfix_of_tuple } from "../internal/postfix_of_tuple"; import { wrap_metadata_rest_tuple } from "../internal/wrap_metadata_rest_tuple"; export namespace JsonStringifyProgrammer { /* ----------------------------------------------------------- WRITER ----------------------------------------------------------- */ export const decompose = (props: { validated: boolean; project: IProject; importer: FunctionImporter; type: ts.Type; name: string | undefined; }): FeatureProgrammer.IDecomposed => { const config: FeatureProgrammer.IConfig = configure(props.project)( props.importer, ); if (props.validated === false) config.addition = (collection) => IsProgrammer.write_function_statements(props.project)(props.importer)( collection, ); const composed: FeatureProgrammer.IComposed = FeatureProgrammer.compose({ ...props, config, }); return { functions: composed.functions, statements: composed.statements, arrow: ts.factory.createArrowFunction( undefined, undefined, composed.parameters, composed.response, undefined, composed.body, ), }; }; export const write = (project: IProject) => (modulo: ts.LeftHandSideExpression) => (type: ts.Type, name?: string): ts.CallExpression => { const importer: FunctionImporter = new FunctionImporter(modulo.getText()); const result: FeatureProgrammer.IDecomposed = decompose({ validated: false, project, importer, type, name, }); return FeatureProgrammer.writeDecomposed({ modulo, importer, result, }); }; const write_array_functions = (config: FeatureProgrammer.IConfig) => (importer: FunctionImporter) => (collection: MetadataCollection): ts.VariableStatement[] => collection .arrays() .filter((a) => a.recursive) .map((type, i) => StatementFactory.constant( `${config.prefix}a${i}`, ts.factory.createArrowFunction( undefined, undefined, FeatureProgrammer.parameterDeclarations(config)( TypeFactory.keyword("any"), )(ts.factory.createIdentifier("input")), TypeFactory.keyword("any"), undefined, decode_array_inline(config)(importer)( ts.factory.createIdentifier("input"), MetadataArray.create({ type, tags: [], }), { tracable: config.trace, source: "function", from: "array", postfix: "", }, ), ), ), ); const write_tuple_functions = (project: IProject) => (config: FeatureProgrammer.IConfig) => (importer: FunctionImporter) => (collection: MetadataCollection): ts.VariableStatement[] => collection .tuples() .filter((t) => t.recursive) .map((tuple, i) => StatementFactory.constant( `${config.prefix}t${i}`, ts.factory.createArrowFunction( undefined, undefined, FeatureProgrammer.parameterDeclarations(config)( TypeFactory.keyword("any"), )(ts.factory.createIdentifier("input")), TypeFactory.keyword("any"), undefined, decode_tuple_inline(project)(config)(importer)( ts.factory.createIdentifier("input"), tuple, { tracable: config.trace, source: "function", from: "array", postfix: "", }, ), ), ), ); /* ----------------------------------------------------------- DECODERS ----------------------------------------------------------- */ const decode = (project: IProject) => (config: FeatureProgrammer.IConfig) => (importer: FunctionImporter) => ( input: ts.Expression, meta: Metadata, explore: FeatureProgrammer.IExplore, ): ts.Expression => { // ANY TYPE if (meta.any === true) return wrap_required( input, meta, explore, )( wrap_functional( input, meta, explore, )( ts.factory.createCallExpression( ts.factory.createIdentifier("JSON.stringify"), undefined, [input], ), ), ); // ONLY NULL OR UNDEFINED const size: number = meta.size(); if ( size === 0 && (meta.isRequired() === false || meta.nullable === true) ) { if (meta.isRequired() === false && meta.nullable === true) return explore.from === "array" ? ts.factory.createStringLiteral("null") : ts.factory.createConditionalExpression( ts.factory.createStrictEquality(ts.factory.createNull(), input), undefined, ts.factory.createStringLiteral("null"), undefined, ts.factory.createIdentifier("undefined"), ); else if (meta.isRequired() === false) return explore.from === "array" ? ts.factory.createStringLiteral("null") : ts.factory.createIdentifier("undefined"); else return ts.factory.createStringLiteral("null"); } //---- // LIST UP UNION TYPES //---- const unions: IUnion[] = []; // toJSON() METHOD if (meta.escaped !== null) unions.push({ type: "resolved", is: meta.escaped.original.size() === 1 && meta.escaped.original.natives[0] === "Date" ? () => check_native("Date")(input) : () => IsProgrammer.decode_to_json(false)(input), value: () => decode_to_json(project)(config)(importer)( input, meta.escaped!.returns, explore, ), }); else if (meta.functions.length) unions.push({ type: "functional", is: () => IsProgrammer.decode_functional(input), value: () => decode_functional(explore), }); // TEMPLATES if ( meta.templates.length || ArrayUtil.has(meta.constants, (c) => c.type === "string") ) if (AtomicPredicator.template(meta)) { const partial = Metadata.initialize(); partial.atomics.push( MetadataAtomic.create({ type: "string", tags: [] }), ), unions.push({ type: "template literal", is: () => IsProgrammer.decode(project)(importer)(input, partial, explore), value: () => decode_atomic(project)(importer)(input, "string", explore), }); } // CONSTANTS for (const constant of meta.constants) if (AtomicPredicator.constant(meta)(constant.type) === false) continue; else if (constant.type !== "string") unions.push({ type: "atomic", is: () => IsProgrammer.decode(project)(importer)( input, (() => { const partial = Metadata.initialize(); partial.atomics.push( MetadataAtomic.create({ type: constant.type, tags: [], }), ); return partial; })(), explore, ), value: () => decode_atomic(project)(importer)(input, constant.type, explore), }); else if (meta.templates.length === 0) unions.push({ type: "const string", is: () => IsProgrammer.decode(project)(importer)( input, (() => { const partial = Metadata.initialize(); partial.atomics.push( MetadataAtomic.create({ type: "string", tags: [], }), ); return partial; })(), explore, ), value: () => decode_constant_string(project)(importer)( input, [...constant.values.map((v) => v.value)] as string[], explore, ), }); /// ATOMICS for (const a of meta.atomics) if (AtomicPredicator.atomic(meta)(a.type)) unions.push({ type: "atomic", is: () => IsProgrammer.decode(project)(importer)( input, (() => { const partial = Metadata.initialize(); partial.atomics.push(a); return partial; })(), explore, ), value: () => decode_atomic(project)(importer)(input, a.type, explore), }); // TUPLES for (const tuple of meta.tuples) unions.push({ type: "tuple", is: () => IsProgrammer.decode(project)(importer)( input, (() => { const partial = Metadata.initialize(); partial.tuples.push(tuple); return partial; })(), explore, ), value: () => decode_tuple(project)(config)(importer)(input, tuple, explore), }); // ARRAYS if (meta.arrays.length) { const value: () => ts.Expression = meta.arrays.length === 1 ? () => decode_array(config)(importer)(input, meta.arrays[0]!, { ...explore, from: "array", }) : meta.arrays.some((elem) => elem.type.value.any) ? () => ts.factory.createCallExpression( ts.factory.createIdentifier("JSON.stringify"), undefined, [input], ) : () => explore_arrays(project)(config)(importer)( input, meta.arrays, { ...explore, from: "array", }, ); unions.push({ type: "array", is: () => ExpressionFactory.isArray(input), value, }); } // BUILT-IN CLASSES if (meta.natives.length) for (const native of meta.natives) unions.push({ type: "object", is: () => check_native(native)(input), value: () => AtomicPredicator.native(native) ? decode_atomic(project)(importer)( input, native.toLowerCase() as Atomic.Literal, explore, ) : ts.factory.createStringLiteral("{}"), }); // SETS if (meta.sets.length) unions.push({ type: "object", is: () => ExpressionFactory.isInstanceOf("Set")(input), value: () => ts.factory.createStringLiteral("{}"), }); // MAPS if (meta.maps.length) unions.push({ type: "object", is: () => ExpressionFactory.isInstanceOf("Map")(input), value: () => ts.factory.createStringLiteral("{}"), }); // OBJECTS if (meta.objects.length) unions.push({ type: "object", is: () => ExpressionFactory.isObject({ checkNull: true, checkArray: meta.objects.some((obj) => obj.properties.every( (prop) => !prop.key.isSoleLiteral() || !prop.value.isRequired(), ), ), })(input), value: () => explore_objects(config)(importer)(input, meta, { ...explore, from: "object", }), }); //---- // RETURNS //---- // CHECK NULL AND UNDEFINED const wrapper = (output: ts.Expression) => wrap_required(input, meta, explore)(wrap_nullable(input, meta)(output)); // DIRECT RETURN if (unions.length === 0) return ts.factory.createCallExpression( ts.factory.createIdentifier("JSON.stringify"), undefined, [input], ); else if (unions.length === 1) return wrapper(unions[0]!.value()); // RETURN WITH TYPE CHECKING return wrapper( ts.factory.createCallExpression( ts.factory.createArrowFunction( undefined, undefined, [], undefined, undefined, iterate(importer, input, unions, meta.getName()), ), undefined, undefined, ), ); }; const decode_object = (importer: FunctionImporter) => FeatureProgrammer.decode_object({ trace: false, path: false, prefix: PREFIX, })(importer); const decode_array = (config: FeatureProgrammer.IConfig) => (importer: FunctionImporter) => ( input: ts.Expression, array: MetadataArray, explore: FeatureProgrammer.IExplore, ) => array.type.recursive ? ts.factory.createCallExpression( ts.factory.createIdentifier( importer.useLocal(`${config.prefix}a${array.type.index}`), ), undefined, FeatureProgrammer.argumentsArray(config)({ ...explore, source: "function", from: "array", })(input), ) : decode_array_inline(config)(importer)(input, array, explore); const decode_array_inline = (config: FeatureProgrammer.IConfig) => (importer: FunctionImporter) => ( input: ts.Expression, array: MetadataArray, explore: FeatureProgrammer.IExplore, ) => FeatureProgrammer.decode_array(config)(importer)(StringifyJoiner.array)( input, array, explore, ); const decode_tuple = (project: IProject) => (config: FeatureProgrammer.IConfig) => (importer: FunctionImporter) => ( input: ts.Expression, tuple: MetadataTuple, explore: FeatureProgrammer.IExplore, ): ts.Expression => tuple.type.recursive ? ts.factory.createCallExpression( ts.factory.createIdentifier( importer.useLocal(`${config.prefix}t${tuple.type.index}`), ), undefined, FeatureProgrammer.argumentsArray(config)({ ...explore, source: "function", })(input), ) : decode_tuple_inline(project)(config)(importer)( input, tuple.type, explore, ); const decode_tuple_inline = (project: IProject) => (config: FeatureProgrammer.IConfig) => (importer: FunctionImporter) => ( input: ts.Expression, tuple: MetadataTupleType, explore: FeatureProgrammer.IExplore, ): ts.Expression => { const children: ts.Expression[] = tuple.elements .filter((elem) => elem.rest === null) .map((elem, index) => decode(project)(config)(importer)( ts.factory.createElementAccessExpression(input, index), elem, { ...explore, from: "array", postfix: explore.postfix.length ? `${postfix_of_tuple(explore.postfix)}[${index}]"` : `"[${index}]"`, }, ), ); const rest = (() => { if (tuple.elements.length === 0) return null; const last = tuple.elements.at(-1)!; if (last.rest === null) return null; const code = decode(project)(config)(importer)( ts.factory.createCallExpression( IdentifierFactory.access(input)("slice"), undefined, [ExpressionFactory.number(tuple.elements.length - 1)], ), wrap_metadata_rest_tuple(tuple.elements.at(-1)!.rest!), { ...explore, start: tuple.elements.length - 1, }, ); return ts.factory.createCallExpression( importer.use("rest"), undefined, [code], ); })(); return StringifyJoiner.tuple(children, rest); }; const decode_atomic = (project: IProject) => (importer: FunctionImporter) => ( input: ts.Expression, type: string, explore: FeatureProgrammer.IExplore, ) => { if (type === "string") return ts.factory.createCallExpression( importer.use("string"), undefined, [input], ); else if (type === "number" && OptionPredicator.numeric(project.options)) input = ts.factory.createCallExpression( importer.use("number"), undefined, [input], ); return explore.from !== "top" ? input : ts.factory.createCallExpression( IdentifierFactory.access(input)("toString"), undefined, undefined, ); }; const decode_constant_string = (project: IProject) => (importer: FunctionImporter) => ( input: ts.Expression, values: string[], explore: FeatureProgrammer.IExplore, ): ts.Expression => { if (values.every((v) => !StringifyPredicator.require_escape(v))) return [ ts.factory.createStringLiteral('"'), input, ts.factory.createStringLiteral('"'), ].reduce((x, y) => ts.factory.createAdd(x, y)); else return decode_atomic(project)(importer)(input, "string", explore); }; const decode_to_json = (project: IProject) => (config: FeatureProgrammer.IConfig) => (importer: FunctionImporter) => ( input: ts.Expression, resolved: Metadata, explore: FeatureProgrammer.IExplore, ): ts.Expression => { return decode(project)(config)(importer)( ts.factory.createCallExpression( IdentifierFactory.access(input)("toJSON"), undefined, [], ), resolved, explore, ); }; const decode_functional = (explore: FeatureProgrammer.IExplore) => explore.from === "array" ? ts.factory.createStringLiteral("null") : ts.factory.createIdentifier("undefined"); /* ----------------------------------------------------------- EXPLORERS ----------------------------------------------------------- */ const explore_objects = (config: FeatureProgrammer.IConfig) => (importer: FunctionImporter) => ( input: ts.Expression, meta: Metadata, explore: FeatureProgrammer.IExplore, ) => meta.objects.length === 1 ? decode_object(importer)(input, meta.objects[0]!, explore) : ts.factory.createCallExpression( ts.factory.createIdentifier( importer.useLocal(`${PREFIX}u${meta.union_index!}`), ), undefined, FeatureProgrammer.argumentsArray(config)(explore)(input), ); const explore_arrays = (project: IProject) => (config: FeatureProgrammer.IConfig) => (importer: FunctionImporter) => ( input: ts.Expression, elements: MetadataArray[], explore: FeatureProgrammer.IExplore, ): ts.Expression => explore_array_like_union_types(config)(importer)( UnionExplorer.array({ checker: IsProgrammer.decode(project)(importer), decoder: decode_array(config)(importer), empty: ts.factory.createStringLiteral("[]"), success: ts.factory.createTrue(), failure: (input, expected) => create_throw_error(importer)(expected)(input), }), )(input, elements, explore); const explore_array_like_union_types = (config: FeatureProgrammer.IConfig) => (importer: FunctionImporter) => <T extends MetadataArray | MetadataTuple>( factory: ( parameters: ts.ParameterDeclaration[], ) => ( input: ts.Expression, elements: T[], explore: FeatureProgrammer.IExplore, ) => ts.ArrowFunction, ) => ( input: ts.Expression, elements: T[], explore: FeatureProgrammer.IExplore, ): ts.Expression => { const arrow = (parameters: ts.ParameterDeclaration[]) => (explore: FeatureProgrammer.IExplore) => (input: ts.Expression): ts.ArrowFunction => factory(parameters)(input, elements, explore); if (elements.every((e) => e.type.recursive === false)) ts.factory.createCallExpression( arrow([])(explore)(input), undefined, [], ); explore = { ...explore, source: "function", from: "array", }; return ts.factory.createCallExpression( ts.factory.createIdentifier( importer.emplaceUnion( config.prefix, elements.map((e) => e.type.name).join(" | "), () => arrow( FeatureProgrammer.parameterDeclarations(config)( TypeFactory.keyword("any"), )(ts.factory.createIdentifier("input")), )({ ...explore, postfix: "", })(ts.factory.createIdentifier("input")), ), ), undefined, FeatureProgrammer.argumentsArray(config)(explore)(input), ); }; /* ----------------------------------------------------------- RETURN SCRIPTS ----------------------------------------------------------- */ const wrap_required = ( input: ts.Expression, meta: Metadata, explore: FeatureProgrammer.IExplore, ): ((expression: ts.Expression) => ts.Expression) => { if (meta.isRequired() === true && meta.any === false) return (expression) => expression; return (expression) => ts.factory.createConditionalExpression( ts.factory.createStrictInequality( ts.factory.createIdentifier("undefined"), input, ), undefined, expression, undefined, explore.from === "array" ? ts.factory.createStringLiteral("null") : ts.factory.createIdentifier("undefined"), ); }; const wrap_nullable = ( input: ts.Expression, meta: Metadata, ): ((expression: ts.Expression) => ts.Expression) => { if (meta.nullable === false) return (expression) => expression; return (expression) => ts.factory.createConditionalExpression( ts.factory.createStrictInequality(ts.factory.createNull(), input), undefined, expression, undefined, ts.factory.createStringLiteral("null"), ); }; const wrap_functional = ( input: ts.Expression, meta: Metadata, explore: FeatureProgrammer.IExplore, ): ((expression: ts.Expression) => ts.Expression) => { if (meta.functions.length === 0) return (expression) => expression; return (expression) => ts.factory.createConditionalExpression( ts.factory.createStrictInequality( ts.factory.createStringLiteral("function"), ValueFactory.TYPEOF(input), ), undefined, expression, undefined, decode_functional(explore), ); }; const iterate = ( importer: FunctionImporter, input: ts.Expression, unions: IUnion[], expected: string, ) => ts.factory.createBlock( [ ...unions.map((u) => ts.factory.createIfStatement( u.is(), ts.factory.createReturnStatement(u.value()), ), ), create_throw_error(importer)(expected)(input), ], true, ); /* ----------------------------------------------------------- CONFIGURATIONS ----------------------------------------------------------- */ const PREFIX = "$s"; const configure = (project: IProject) => (importer: FunctionImporter): FeatureProgrammer.IConfig => { const config: FeatureProgrammer.IConfig = { types: { input: (type, name) => ts.factory.createTypeReferenceNode( name ?? TypeFactory.getFullName(project.checker)(type), ), output: () => TypeFactory.keyword("string"), }, prefix: PREFIX, trace: false, path: false, initializer, decoder: () => decode(project)(config)(importer), objector: { checker: () => IsProgrammer.decode(project)(importer), decoder: () => decode_object(importer), joiner: StringifyJoiner.object(importer), unionizer: decode_union_object( IsProgrammer.decode_object(project)(importer), )(decode_object(importer))((exp) => exp)((value, expected) => create_throw_error(importer)(expected)(value), ), failure: (input, expected) => create_throw_error(importer)(expected)(input), }, generator: { arrays: () => write_array_functions(config)(importer), tuples: () => write_tuple_functions(project)(config)(importer), }, }; return config; }; const initializer: FeatureProgrammer.IConfig["initializer"] = (project) => (importer) => (type) => JsonMetadataFactory.analyze(`typia.json.${importer.method}`)( project.checker, project.context, )(type); const create_throw_error = (importer: FunctionImporter) => (expected: string) => (value: ts.Expression) => ts.factory.createExpressionStatement( ts.factory.createCallExpression( importer.use("throws"), [], [ ts.factory.createObjectLiteralExpression( [ ts.factory.createPropertyAssignment( "expected", ts.factory.createStringLiteral(expected), ), ts.factory.createPropertyAssignment("value", value), ], true, ), ], ), ); } interface IUnion { type: string; is: () => ts.Expression; value: () => ts.Expression; }