UNPKG

typia

Version:

Superfast runtime validators with only one line

946 lines (914 loc) 29.4 kB
import ts from "typescript"; import { ExpressionFactory } from "../../factories/ExpressionFactory"; import { IdentifierFactory } from "../../factories/IdentifierFactory"; import { MetadataCollection } from "../../factories/MetadataCollection"; import { NumericRangeFactory } from "../../factories/NumericRangeFactory"; import { ProtobufFactory } from "../../factories/ProtobufFactory"; import { StatementFactory } from "../../factories/StatementFactory"; import { TypeFactory } from "../../factories/TypeFactory"; import { Metadata } from "../../schemas/metadata/Metadata"; import { MetadataMap } from "../../schemas/metadata/MetadataMap"; import { MetadataObjectType } from "../../schemas/metadata/MetadataObjectType"; import { IProtobufProperty } from "../../schemas/protobuf/IProtobufProperty"; import { IProtobufPropertyType } from "../../schemas/protobuf/IProtobufPropertyType"; import { IProtobufSchema } from "../../schemas/protobuf/IProtobufSchema"; import { IProgrammerProps } from "../../transformers/IProgrammerProps"; import { ITypiaContext } from "../../transformers/ITypiaContext"; import { ProtobufAtomic } from "../../typings/ProtobufAtomic"; import { FeatureProgrammer } from "../FeatureProgrammer"; import { IsProgrammer } from "../IsProgrammer"; import { FunctionProgrammer } from "../helpers/FunctionProgrammer"; import { ProtobufUtil } from "../helpers/ProtobufUtil"; import { ProtobufWire } from "../helpers/ProtobufWire"; import { UnionPredicator } from "../helpers/UnionPredicator"; import { decode_union_object } from "../internal/decode_union_object"; export namespace ProtobufEncodeProgrammer { export const decompose = (props: { context: ITypiaContext; modulo: ts.LeftHandSideExpression; functor: FunctionProgrammer; type: ts.Type; name: string | undefined; }): FeatureProgrammer.IDecomposed => { const collection: MetadataCollection = new MetadataCollection(); const metadata: Metadata = ProtobufFactory.metadata({ method: props.modulo.getText(), checker: props.context.checker, transformer: props.context.transformer, collection, type: props.type, }); const callEncoder = (writer: string, factory: ts.NewExpression) => StatementFactory.constant({ name: writer, value: ts.factory.createCallExpression( ts.factory.createIdentifier("encoder"), undefined, [factory, ts.factory.createIdentifier("input")], ), }); return { functions: { encoder: StatementFactory.constant({ name: props.functor.useLocal("encoder"), value: write_encoder({ context: props.context, functor: props.functor, collection, metadata, }), }), }, statements: [], arrow: ts.factory.createArrowFunction( undefined, undefined, [ IdentifierFactory.parameter( "input", ts.factory.createTypeReferenceNode( props.name ?? TypeFactory.getFullName({ checker: props.context.checker, type: props.type, }), ), ), ], ts.factory.createTypeReferenceNode("Uint8Array"), undefined, ts.factory.createBlock( [ callEncoder( "sizer", ts.factory.createNewExpression( props.context.importer.internal("ProtobufSizer"), undefined, [], ), ), callEncoder( "writer", ts.factory.createNewExpression( props.context.importer.internal("ProtobufWriter"), undefined, [ts.factory.createIdentifier("sizer")], ), ), ts.factory.createReturnStatement(callWriter("buffer")), ], true, ), ), }; }; export const write = (props: IProgrammerProps): ts.CallExpression => { const functor: FunctionProgrammer = new FunctionProgrammer( props.modulo.getText(), ); const result: FeatureProgrammer.IDecomposed = decompose({ ...props, functor, }); return FeatureProgrammer.writeDecomposed({ modulo: props.modulo, functor, result, }); }; const write_encoder = (props: { context: ITypiaContext; functor: FunctionProgrammer; collection: MetadataCollection; metadata: Metadata; }): ts.ArrowFunction => { const functors = props.collection .objects() .filter((obj) => ProtobufUtil.isStaticObject(obj)) .map((object) => StatementFactory.constant({ name: `${PREFIX}o${object.index}`, value: write_object_function({ context: props.context, functor: props.functor, input: ts.factory.createIdentifier("input"), object, explore: { source: "function", from: "object", tracable: false, postfix: "", }, }), }), ); return ts.factory.createArrowFunction( undefined, [ ts.factory.createTypeParameterDeclaration( undefined, "Writer", props.context.importer.type({ file: "typia/lib/internal/_IProtobufWriter.js", name: "_IProtobufWriter", }), ), ], [ IdentifierFactory.parameter( "writer", ts.factory.createTypeReferenceNode("Writer"), ), IdentifierFactory.parameter("input"), ], ts.factory.createTypeReferenceNode("Writer"), undefined, ts.factory.createBlock( [ ...props.functor.declareUnions(), ...functors, ...IsProgrammer.write_function_statements(props), ts.factory.createExpressionStatement( ts.factory.createCallExpression( ts.factory.createIdentifier( props.functor.useLocal( `${PREFIX}o${props.metadata.objects[0]?.type.index ?? 0}`, ), ), [], [ts.factory.createIdentifier("input")], ), ), ts.factory.createReturnStatement( ts.factory.createIdentifier("writer"), ), ], true, ), ); }; const write_object_function = (props: { context: ITypiaContext; functor: FunctionProgrammer; input: ts.Expression; object: MetadataObjectType; explore: FeatureProgrammer.IExplore; }): ts.ArrowFunction => { const body: ts.Statement[] = props.object.properties .map((p) => { const block = decode_property({ context: props.context, functor: props.functor, explore: props.explore, metadata: p.value, protobuf: p.of_protobuf_!, input: IdentifierFactory.access(props.input, p.key.getSoleLiteral()!), }); return [ ts.factory.createExpressionStatement( ts.factory.createIdentifier( `// property ${JSON.stringify(p.key.getSoleLiteral())}: ${p.value.getName()}`, ), ), ...block.statements, ]; }) .flat(); return ts.factory.createArrowFunction( undefined, undefined, [IdentifierFactory.parameter("input")], TypeFactory.keyword("any"), undefined, ts.factory.createBlock(body, true), ); }; /* ----------------------------------------------------------- DECODER STATION ----------------------------------------------------------- */ const decode_property = (props: { context: ITypiaContext; functor: FunctionProgrammer; metadata: Metadata; protobuf: IProtobufProperty; input: ts.Expression; explore: FeatureProgrammer.IExplore; }): ts.Block => { const union: IUnion[] = []; for (const schema of props.protobuf.union) { //---- // ATOMICS //---- if (schema.type === "bool") union.push({ is: () => ts.factory.createStrictEquality( ts.factory.createStringLiteral("boolean"), ts.factory.createTypeOfExpression(props.input), ), value: () => decode_bool({ input: props.input, index: schema.index, }), }); else if (schema.type === "bigint") union.push( decode_bigint({ input: props.input, type: schema.name, candidates: props.protobuf.union .filter((s) => s.type === "bigint") .map((s) => s.name), index: schema.index, }), ); else if (schema.type === "number") union.push( decode_number({ input: props.input, type: schema.name, candidates: props.protobuf.union .filter((s) => s.type === "number") .map((s) => s.name), index: schema.index, }), ); else if (schema.type === "string") union.push({ is: () => ts.factory.createStrictEquality( ts.factory.createStringLiteral("string"), ts.factory.createTypeOfExpression(props.input), ), value: () => decode_bytes({ method: "string", index: schema.index, input: props.input, }), }); //---- // INSTANCES //---- else if (schema.type === "bytes") union.push({ is: () => ExpressionFactory.isInstanceOf("Uint8Array", props.input), value: () => decode_bytes({ method: "bytes", index: schema.index, input: props.input, }), }); else if (schema.type === "array") union.push({ is: () => ExpressionFactory.isArray(props.input), value: () => decode_array({ context: props.context, functor: props.functor, input: props.input, schema, }), }); else if (schema.type === "map" && schema.map instanceof MetadataMap) { union.push({ is: () => ExpressionFactory.isInstanceOf("Map", props.input), value: () => decode_map({ context: props.context, functor: props.functor, schema, input: props.input, }), }); } const objectSchemas: Array< IProtobufPropertyType.IObject | IProtobufPropertyType.IMap > = props.protobuf.union .filter((schema) => schema.type === "object" || schema.type === "map") .filter( (schema) => schema.type === "object" || (schema.type === "map" && schema.map instanceof MetadataObjectType), ); if (objectSchemas.length !== 0) union.push({ is: () => ExpressionFactory.isObject({ checkNull: true, checkArray: false, input: props.input, }), value: () => explore_objects({ context: props.context, functor: props.functor, level: 0, schemas: objectSchemas, explore: { ...props.explore, from: "object", }, input: props.input, }), }); } // RETURNS const wrapper: (block: ts.Block) => ts.Block = props.metadata.isRequired() && props.metadata.nullable === false ? (block) => block : props.metadata.isRequired() === false && props.metadata.nullable === true ? (block) => ts.factory.createBlock( [ ts.factory.createIfStatement( ts.factory.createLogicalAnd( ts.factory.createStrictInequality( ts.factory.createIdentifier("undefined"), props.input, ), ts.factory.createStrictInequality( ts.factory.createNull(), props.input, ), ), block, ), ], true, ) : props.metadata.isRequired() === false ? (block) => ts.factory.createBlock( [ ts.factory.createIfStatement( ts.factory.createStrictInequality( ts.factory.createIdentifier("undefined"), props.input, ), block, ), ], true, ) : (block) => ts.factory.createBlock( [ ts.factory.createIfStatement( ts.factory.createStrictInequality( ts.factory.createNull(), props.input, ), block, ), ], true, ); if (union.length === 1) return wrapper(union[0]!.value()); return wrapper( ts.factory.createBlock( [ union .map((u, i) => ts.factory.createIfStatement( u.is(), u.value(), i === union.length - 1 ? create_throw_error({ context: props.context, functor: props.functor, input: props.input, expected: props.metadata.getName(), }) : undefined, ), ) .reverse() .reduce((a, b) => ts.factory.createIfStatement(b.expression, b.thenStatement, a), ), ], true, ), ); }; /* ----------------------------------------------------------- ATOMIC DECODERS ----------------------------------------------------------- */ const decode_bool = (props: { input: ts.Expression; index: number | null; }): ts.Block => ts.factory.createBlock( [ ...(props.index !== null ? [ decode_tag({ wire: ProtobufWire.VARIANT, index: props.index, }), ] : []), callWriter("bool", [props.input]), ].map((exp) => ts.factory.createExpressionStatement(exp)), true, ); const decode_bigint = (props: { candidates: ProtobufAtomic.BigNumeric[]; type: ProtobufAtomic.BigNumeric; input: ts.Expression; index: number | null; }): IUnion => ({ is: () => props.candidates.length === 1 ? ts.factory.createStrictEquality( ts.factory.createStringLiteral("bigint"), ts.factory.createTypeOfExpression(props.input), ) : ts.factory.createLogicalAnd( ts.factory.createStrictEquality( ts.factory.createStringLiteral("bigint"), ts.factory.createTypeOfExpression(props.input), ), NumericRangeFactory.bigint(props.type, props.input), ), value: () => ts.factory.createBlock( [ ...(props.index !== null ? [ decode_tag({ wire: ProtobufWire.VARIANT, index: props.index, }), ] : []), callWriter(props.type, [props.input]), ].map((exp) => ts.factory.createExpressionStatement(exp)), true, ), }); const decode_number = (props: { candidates: ProtobufAtomic.Numeric[]; type: ProtobufAtomic.Numeric; input: ts.Expression; index: number | null; }): IUnion => ({ is: () => props.candidates.length === 1 ? ts.factory.createStrictEquality( ts.factory.createStringLiteral("number"), ts.factory.createTypeOfExpression(props.input), ) : ts.factory.createLogicalAnd( ts.factory.createStrictEquality( ts.factory.createStringLiteral("number"), ts.factory.createTypeOfExpression(props.input), ), NumericRangeFactory.number(props.type, props.input), ), value: () => ts.factory.createBlock( [ ...(props.index !== null ? [ decode_tag({ wire: get_numeric_wire(props.type), index: props.index, }), ] : []), callWriter(props.type, [props.input]), ].map((exp) => ts.factory.createExpressionStatement(exp)), true, ), }); const decode_container_value = (props: { context: ITypiaContext; functor: FunctionProgrammer; schema: IProtobufPropertyType.IArray["value"]; index: number; kind: "array" | "map"; input: ts.Expression; }): ts.Block => { if (props.schema.type === "bool") return decode_bool({ input: props.input, index: props.kind === "array" ? null : props.index, }); else if (props.schema.type === "bigint") return decode_bigint({ input: props.input, type: props.schema.name, candidates: [props.schema.name], index: props.kind === "array" ? null : props.index, }).value(); else if (props.schema.type === "number") return decode_number({ input: props.input, type: props.schema.name, candidates: [props.schema.name], index: props.kind === "array" ? null : props.index, }).value(); else if (props.schema.type === "string" || props.schema.type === "bytes") return decode_bytes({ method: props.schema.type, input: props.input, index: props.index, }); return decode_object({ context: props.context, functor: props.functor, schema: props.schema, input: props.input, index: props.index, }); }; /* ----------------------------------------------------------- INSTANCE DECODERS ----------------------------------------------------------- */ const decode_bytes = (props: { method: "bytes" | "string"; index: number; input: ts.Expression; }): ts.Block => ts.factory.createBlock( [ decode_tag({ wire: ProtobufWire.LEN, index: props.index, }), callWriter(props.method, [props.input]), ].map((expr) => ts.factory.createExpressionStatement(expr)), true, ); const decode_array = (props: { context: ITypiaContext; functor: FunctionProgrammer; schema: IProtobufPropertyType.IArray; input: ts.Expression; }): ts.Block => { const value: IProtobufPropertyType.IArray["value"] = props.schema.value; const wire: ProtobufWire = (() => { if ( value.type === "object" || value.type === "bytes" || value.type === "string" ) return ProtobufWire.LEN; else if (value.type === "number" && value.name === "float") return ProtobufWire.I32; return ProtobufWire.VARIANT; })(); const forLoop = () => ts.factory.createForOfStatement( undefined, ts.factory.createVariableDeclarationList( [ts.factory.createVariableDeclaration("elem")], ts.NodeFlags.Const, ), props.input, decode_container_value({ kind: "array", context: props.context, functor: props.functor, input: ts.factory.createIdentifier("elem"), index: props.schema.index, schema: props.schema.value, }), ); const length = (block: ts.Block) => ts.factory.createBlock( [ ts.factory.createIfStatement( ts.factory.createStrictInequality( ExpressionFactory.number(0), IdentifierFactory.access(props.input, "length"), ), block, ), ], true, ); if (wire === ProtobufWire.LEN) return length(ts.factory.createBlock([forLoop()], true)); return length( ts.factory.createBlock( [ ts.factory.createExpressionStatement( decode_tag({ wire: ProtobufWire.LEN, index: props.schema.index, }), ), ts.factory.createExpressionStatement(callWriter("fork")), forLoop(), ts.factory.createExpressionStatement(callWriter("ldelim")), ], true, ), ); }; const decode_object = (props: { context: ITypiaContext; functor: FunctionProgrammer; schema: IProtobufSchema.IObject; index: number; input: ts.Expression; }): ts.Block => ts.factory.createBlock( [ decode_tag({ wire: ProtobufWire.LEN, index: props.index, }), callWriter("fork"), ts.factory.createCallExpression( ts.factory.createIdentifier( props.functor.useLocal(`${PREFIX}o${props.schema.object.index}`), ), [], [props.input], ), callWriter("ldelim"), ].map(ts.factory.createExpressionStatement), true, ); const decode_map = (props: { context: ITypiaContext; functor: FunctionProgrammer; schema: IProtobufPropertyType.IMap; input: ts.Expression; }): ts.Block => { const each: ts.Statement[] = [ ts.factory.createExpressionStatement( decode_tag({ wire: ProtobufWire.LEN, index: props.schema.index, }), ), ts.factory.createExpressionStatement(callWriter("fork")), ...decode_container_value({ kind: "map", context: props.context, functor: props.functor, index: 1, input: ts.factory.createIdentifier("key"), schema: props.schema.key, }).statements, ...decode_container_value({ kind: "map", context: props.context, functor: props.functor, index: 2, input: ts.factory.createIdentifier("value"), schema: props.schema.value, }).statements, ts.factory.createExpressionStatement(callWriter("ldelim")), ]; return ts.factory.createBlock( [ ts.factory.createForOfStatement( undefined, StatementFactory.entry({ key: "key", value: "value", }), props.input, ts.factory.createBlock(each), ), ], true, ); }; const explore_objects = (props: { context: ITypiaContext; functor: FunctionProgrammer; level: number; input: ts.Expression; schemas: Array<IProtobufPropertyType.IObject | IProtobufPropertyType.IMap>; explore: FeatureProgrammer.IExplore; }): ts.Block => { const out = ( schema: IProtobufPropertyType.IObject | IProtobufPropertyType.IMap, ) => schema.type === "object" ? decode_object({ context: props.context, functor: props.functor, schema, index: schema.index, input: props.input, }) : decode_map({ context: props.context, functor: props.functor, schema, input: ts.factory.createCallExpression( IdentifierFactory.access( ts.factory.createIdentifier("Object"), "entries", ), undefined, [props.input], ), }); if (props.schemas.length === 1) return out(props.schemas[0]!); const objects: MetadataObjectType[] = props.schemas.map((s) => s.type === "map" ? (s.map as MetadataObjectType) : s.object, ); const expected: string = `(${objects.map((t) => t.name).join(" | ")})`; const indexes: WeakMap< MetadataObjectType, IProtobufPropertyType.IObject | IProtobufPropertyType.IMap > = new WeakMap(objects.map((o, i) => [o, props.schemas[i]!])); const specifications: UnionPredicator.ISpecialized[] = UnionPredicator.object(objects); if (specifications.length === 0) { const condition: ts.Expression = decode_union_object({ checker: (v) => IsProgrammer.decode_object({ context: props.context, functor: props.functor, object: v.object, input: v.input, explore: v.explore, }), decoder: (v) => ExpressionFactory.selfCall(out(indexes.get(v.object)!)), success: (expr) => expr, escaper: (v) => create_throw_error({ context: props.context, functor: props.functor, expected: v.expected, input: v.input, }), input: props.input, explore: props.explore, objects, }); return StatementFactory.block(condition); } const remained: MetadataObjectType[] = objects.filter( (o) => specifications.find((s) => s.object === o) === undefined, ); // DO SPECIALIZE const condition: ts.IfStatement = specifications .filter((spec) => spec.property.key.getSoleLiteral() !== null) .map((spec, i, array) => { const key: string = spec.property.key.getSoleLiteral()!; const accessor: ts.Expression = IdentifierFactory.access( props.input, key, ); const pred: ts.Expression = spec.neighbor ? IsProgrammer.decode({ context: props.context, functor: props.functor, input: accessor, metadata: spec.property.value, explore: { ...props.explore, tracable: false, postfix: IdentifierFactory.postfix(key), }, }) : ExpressionFactory.isRequired(accessor); const schema = indexes.get(spec.object)!; return ts.factory.createIfStatement( pred, ts.factory.createExpressionStatement( ExpressionFactory.selfCall(out(schema)), ), i === array.length - 1 ? remained.length ? ts.factory.createExpressionStatement( ExpressionFactory.selfCall( explore_objects({ context: props.context, functor: props.functor, level: props.level + 1, input: props.input, schemas: remained.map((r) => indexes.get(r)!), explore: props.explore, }), ), ) : create_throw_error({ context: props.context, functor: props.functor, input: props.input, expected, }) : undefined, ); }) .reverse() .reduce((a, b) => ts.factory.createIfStatement(b.expression, b.thenStatement, a), ); // RETURNS WITH CONDITIONS return ts.factory.createBlock([condition], true); }; /* ----------------------------------------------------------- BACKGROUND FUNCTIONS ----------------------------------------------------------- */ const PREFIX = "_pe"; const decode_tag = (props: { wire: ProtobufWire; index: number; }): ts.CallExpression => callWriter("uint32", [ ExpressionFactory.number((props.index << 3) | props.wire), ]); const get_numeric_wire = (type: ProtobufAtomic.Numeric) => type === "double" ? ProtobufWire.I64 : type === "float" ? ProtobufWire.I32 : ProtobufWire.VARIANT; const create_throw_error = (props: { context: ITypiaContext; functor: FunctionProgrammer; expected: string; input: ts.Expression; }) => ts.factory.createExpressionStatement( ts.factory.createCallExpression( props.context.importer.internal("throwTypeGuardError"), [], [ ts.factory.createObjectLiteralExpression( [ ts.factory.createPropertyAssignment( "method", ts.factory.createStringLiteral(props.functor.method), ), ts.factory.createPropertyAssignment( "expected", ts.factory.createStringLiteral(props.expected), ), ts.factory.createPropertyAssignment("value", props.input), ], true, ), ], ), ); } const callWriter = ( method: string, args?: ts.Expression[], ): ts.CallExpression => ts.factory.createCallExpression( IdentifierFactory.access(ts.factory.createIdentifier("writer"), method), undefined, args, ); interface IUnion { is: () => ts.Expression; value: () => ts.Block; }