UNPKG

typia

Version:

Superfast runtime validators with only one line

655 lines (637 loc) 21.2 kB
import ts from "typescript"; import { ExpressionFactory } from "../../factories/ExpressionFactory"; import { IdentifierFactory } from "../../factories/IdentifierFactory"; import { MetadataCollection } from "../../factories/MetadataCollection"; import { MetadataFactory } from "../../factories/MetadataFactory"; import { ProtobufFactory } from "../../factories/ProtobufFactory"; import { StatementFactory } from "../../factories/StatementFactory"; import { TypeFactory } from "../../factories/TypeFactory"; import { Metadata } from "../../schemas/metadata/Metadata"; import { MetadataObjectType } from "../../schemas/metadata/MetadataObjectType"; import { MetadataProperty } from "../../schemas/metadata/MetadataProperty"; 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 { FeatureProgrammer } from "../FeatureProgrammer"; import { FunctionProgrammer } from "../helpers/FunctionProgrammer"; import { ProtobufUtil } from "../helpers/ProtobufUtil"; export namespace ProtobufDecodeProgrammer { 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 meta: Metadata = ProtobufFactory.metadata({ method: props.modulo.getText(), checker: props.context.checker, transformer: props.context.transformer, collection, type: props.type, }); return { functions: Object.fromEntries( collection .objects() .filter((object) => ProtobufUtil.isStaticObject(object)) .map((object) => [ `${PREFIX}o${object.index}`, StatementFactory.constant({ name: props.functor.useLocal(`${PREFIX}o${object.index}`), value: write_object_function({ context: props.context, functor: props.functor, object, }), }), ]), ), statements: [], arrow: ts.factory.createArrowFunction( undefined, undefined, [ IdentifierFactory.parameter( "input", ts.factory.createTypeReferenceNode("Uint8Array"), ), ], props.context.importer.type({ file: "typia", name: "Resolved", arguments: [ ts.factory.createTypeReferenceNode( props.name ?? TypeFactory.getFullName({ checker: props.context.checker, type: props.type, }), ), ], }), undefined, ts.factory.createBlock( [ StatementFactory.constant({ name: "reader", value: ts.factory.createNewExpression( props.context.importer.internal("ProtobufReader"), undefined, [ts.factory.createIdentifier("input")], ), }), ts.factory.createReturnStatement( decode_regular_object({ top: true, object: meta.objects[0]!.type, }), ), ], 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_object_function = (props: { context: ITypiaContext; functor: FunctionProgrammer; object: MetadataObjectType; }): ts.ArrowFunction => ts.factory.createArrowFunction( undefined, undefined, [ IdentifierFactory.parameter("reader"), IdentifierFactory.parameter( "length", TypeFactory.keyword("number"), ExpressionFactory.number(-1), ), ], TypeFactory.keyword("any"), undefined, ts.factory.createBlock( [ ts.factory.createExpressionStatement( ts.factory.createBinaryExpression( ts.factory.createIdentifier("length"), ts.factory.createToken(ts.SyntaxKind.EqualsToken), ts.factory.createConditionalExpression( ts.factory.createLessThan( ts.factory.createIdentifier("length"), ExpressionFactory.number(0), ), undefined, callReader("size"), undefined, ts.factory.createAdd( callReader("index"), ts.factory.createIdentifier("length"), ), ), ), ), ...write_object_function_body({ context: props.context, condition: ts.factory.createLessThan( callReader("index"), ts.factory.createIdentifier("length"), ), tag: "tag", output: "output", object: props.object, }), ts.factory.createReturnStatement( ts.factory.createIdentifier("output"), ), ], true, ), ); const write_object_function_body = (props: { context: ITypiaContext; condition: ts.Expression; tag: string; output: string; object: MetadataObjectType; }): ts.Statement[] => { if (props.object.properties.some((p) => p.of_protobuf_ === undefined)) ProtobufFactory.emplaceObject(props.object); const clauses: ts.CaseClause[] = props.object.properties .map((p) => decode_property({ context: props.context, accessor: IdentifierFactory.access( ts.factory.createIdentifier(props.output), p.key.getSoleLiteral()!, ), protobuf: p.of_protobuf_!, metadata: p.value, }), ) .flat(); return [ StatementFactory.constant({ name: props.output, value: ts.factory.createAsExpression( ts.factory.createObjectLiteralExpression( props.object.properties .filter( (p) => !( props.context.compilerOptions.exactOptionalPropertyTypes === true && p.value.optional === true ), ) .map((p) => ts.factory.createPropertyAssignment( IdentifierFactory.identifier(p.key.getSoleLiteral()!), write_property_default_value(p.value), ), ), true, ), TypeFactory.keyword("any"), ), }), ts.factory.createWhileStatement( props.condition, ts.factory.createBlock([ StatementFactory.constant({ name: props.tag, value: callReader("uint32"), }), ts.factory.createSwitchStatement( ts.factory.createUnsignedRightShift( ts.factory.createIdentifier(props.tag), ExpressionFactory.number(3), ), ts.factory.createCaseBlock([ ...clauses, ts.factory.createDefaultClause([ ts.factory.createExpressionStatement( callReader("skipType", [ ts.factory.createBitwiseAnd( ts.factory.createIdentifier(props.tag), ExpressionFactory.number(7), ), ]), ), ts.factory.createBreakStatement(), ]), ]), ), ]), ), ]; }; const write_property_default_value = (value: Metadata) => ts.factory.createAsExpression( value.nullable ? ts.factory.createNull() : value.isRequired() === false ? ts.factory.createIdentifier("undefined") : value.arrays.length ? ts.factory.createArrayLiteralExpression() : value.maps.length ? ts.factory.createNewExpression( ts.factory.createIdentifier("Map"), undefined, [], ) : value.natives.length ? ts.factory.createNewExpression( ts.factory.createIdentifier("Uint8Array"), undefined, [ts.factory.createArrayLiteralExpression([])], ) : value.atomics.some((a) => a.type === "string") || value.constants.some( (c) => c.type === "string" && c.values.some((v) => v.value === ""), ) || value.templates.some( (tpl) => tpl.row.length === 1 && tpl.row[0]!.getName() === "string", ) ? ts.factory.createStringLiteral("") : value.objects.length && value.objects.some( (obj) => !ProtobufUtil.isStaticObject(obj.type), ) ? ts.factory.createObjectLiteralExpression() : ts.factory.createIdentifier("undefined"), TypeFactory.keyword("any"), ); /* ----------------------------------------------------------- DECODERS ----------------------------------------------------------- */ const decode_property = (props: { context: ITypiaContext; metadata: Metadata; protobuf: IProtobufProperty; accessor: ts.Expression; }): ts.CaseClause[] => props.protobuf.union.map((schema) => decode_property_type({ context: props.context, accessor: props.accessor, schema, required: props.metadata.isRequired() && props.metadata.nullable === false, }), ); const decode_property_type = (props: { context: ITypiaContext; accessor: ts.Expression; schema: IProtobufPropertyType; required: boolean; }): ts.CaseClause => { const out = ( title: string, value: ts.Expression | ts.Statement[], ): ts.CaseClause => ts.factory.createCaseClause( ExpressionFactory.number(props.schema.index), [ ts.factory.createExpressionStatement( ts.factory.createIdentifier(`// ${title}`), ), ...(Array.isArray(value) ? value : [ ts.factory.createExpressionStatement( ts.factory.createBinaryExpression( props.accessor, ts.factory.createToken(ts.SyntaxKind.EqualsToken), value, ), ), ]), ts.factory.createBreakStatement(), ], ); // ATOMICS if (props.schema.type === "bytes") return out("bytes", callReader("bytes")); else if (props.schema.type === "bool") return out("bool", callReader("bool")); else if (props.schema.type === "bigint") return out(props.schema.name, callReader(props.schema.name)); else if (props.schema.type === "number") return out(props.schema.name, decode_number(props.schema)); else if (props.schema.type === "string") return out("string", callReader("string")); // INSTANCES else if (props.schema.type === "array") return out( `Array<${props.schema.array.value.getName()}>`, decode_array({ accessor: props.accessor, schema: props.schema, required: props.required, }), ); else if (props.schema.type === "object") return out( props.schema.object.name, decode_regular_object({ top: false, object: props.schema.object, }), ); else if (props.schema.type === "map") if (props.schema.map instanceof MetadataObjectType) { const key: Metadata = props.schema.map.properties[0]!.key; const value: Metadata = props.schema.map.properties[0]!.value; return out( `Record<${key.getName()}, ${value.getName()}>`, decode_map({ context: props.context, accessor: props.accessor, schema: props.schema, required: props.required, key, value, initializer: ts.factory.createObjectLiteralExpression([]), setter: () => ts.factory.createBinaryExpression( ts.factory.createElementAccessExpression( props.accessor, ts.factory.createIdentifier("entry.key"), ), ts.factory.createToken(ts.SyntaxKind.EqualsToken), ts.factory.createIdentifier("entry.value"), ), }), ); } else { const key: Metadata = props.schema.map.key; const value: Metadata = props.schema.map.value; return out( `Map<${key.getName()}, ${value.getName()}>`, decode_map({ context: props.context, accessor: props.accessor, schema: props.schema, required: props.required, key, value, initializer: ts.factory.createNewExpression( ts.factory.createIdentifier("Map"), [TypeFactory.keyword("any"), TypeFactory.keyword("any")], [], ), setter: () => ts.factory.createCallExpression( IdentifierFactory.access(props.accessor, "set"), undefined, [ ts.factory.createIdentifier("entry.key"), ts.factory.createIdentifier("entry.value"), ], ), }), ); } throw new Error( "Error on ProtobufDecodeProgrammer.write(): unknown property type", ); }; const decode_number = (schema: IProtobufSchema.INumber): ts.Expression => { const value = callReader(schema.name); return schema.name === "int64" || schema.name === "uint64" ? ts.factory.createCallExpression( ts.factory.createIdentifier("Number"), undefined, [value], ) : value; }; const decode_array = (props: { accessor: ts.Expression; schema: IProtobufSchema.IArray; required: boolean; }): ts.Statement[] => { const statements: Array<ts.Expression | ts.Statement> = []; if (props.required === false) statements.push( ts.factory.createBinaryExpression( props.accessor, ts.factory.createToken(ts.SyntaxKind.QuestionQuestionEqualsToken), ts.factory.createAsExpression( ts.factory.createArrayLiteralExpression(), ts.factory.createTypeReferenceNode("any[]"), ), ), ); const decoder: ts.Expression = decode_array_value(props.schema.value); if (["bool", "bigint", "number"].includes(props.schema.value.type)) { statements.push( ts.factory.createIfStatement( ts.factory.createStrictEquality( ExpressionFactory.number(2), ts.factory.createBitwiseAnd( ts.factory.createIdentifier("tag"), ExpressionFactory.number(7), ), ), ts.factory.createBlock( [ StatementFactory.constant({ name: "piece", value: ts.factory.createAdd( callReader("uint32"), callReader("index"), ), }), ts.factory.createWhileStatement( ts.factory.createLessThan( callReader("index"), ts.factory.createIdentifier("piece"), ), ts.factory.createExpressionStatement( ts.factory.createCallExpression( IdentifierFactory.access(props.accessor, "push"), undefined, [decoder], ), ), ), ], true, ), ts.factory.createExpressionStatement( ts.factory.createCallExpression( IdentifierFactory.access(props.accessor, "push"), undefined, [decoder], ), ), ), ); } else statements.push( ts.factory.createCallExpression( IdentifierFactory.access(props.accessor, "push"), undefined, [decoder], ), ); return statements.map((stmt) => ts.isExpression(stmt) ? ts.factory.createExpressionStatement(stmt) : stmt, ); }; const decode_regular_object = (props: { top: boolean; object: MetadataObjectType; }): ts.Expression => ts.factory.createCallExpression( ts.factory.createIdentifier(`${PREFIX}o${props.object.index}`), undefined, [ ts.factory.createIdentifier("reader"), ...(props.top ? [] : [callReader("uint32")]), ], ); const decode_map = (props: { context: ITypiaContext; accessor: ts.Expression; key: Metadata; value: Metadata; schema: IProtobufSchema.IMap; initializer: ts.Expression; required: boolean; setter: () => ts.Expression; }): ts.Statement[] => { const statements: Array<ts.Expression | ts.Statement> = [ ...(props.required === true ? [ ts.factory.createBinaryExpression( props.accessor, ts.factory.createToken(ts.SyntaxKind.QuestionQuestionEqualsToken), props.initializer, ), ] : []), StatementFactory.constant({ name: "piece", value: ts.factory.createAdd(callReader("uint32"), callReader("index")), }), ...write_object_function_body({ context: props.context, condition: ts.factory.createLessThan( callReader("index"), ts.factory.createIdentifier("piece"), ), tag: "kind", output: "entry", object: createObjectType([ { key: "key", value: props.key, }, { key: "value", value: props.value, }, ]), }), ...(props.required === false ? [ ts.factory.createBinaryExpression( props.accessor, ts.factory.createToken(ts.SyntaxKind.QuestionQuestionEqualsToken), props.initializer, ), ] : []), props.setter(), ]; return [ ts.factory.createExpressionStatement( ExpressionFactory.selfCall( ts.factory.createBlock( statements.map((stmt) => ts.isExpression(stmt) ? ts.factory.createExpressionStatement(stmt) : stmt, ), true, ), ), ), ]; }; const decode_array_value = ( schema: IProtobufSchema.IArray["value"], ): ts.Expression => { if (schema.type === "bytes") return callReader("bytes"); else if (schema.type === "bool") return callReader("bool"); else if (schema.type === "bigint") return callReader(schema.name); else if (schema.type === "number") return decode_number(schema); else if (schema.type === "string") return callReader("string"); else if (schema.type === "object") return decode_regular_object({ top: false, object: schema.object, }); throw new Error("unreachable condition"); }; } const PREFIX = "_pd"; const callReader = ( method: string, args?: ts.Expression[], ): ts.CallExpression => ts.factory.createCallExpression( IdentifierFactory.access(ts.factory.createIdentifier("reader"), method), undefined, args, ); const createObjectType = ( definitions: Array<{ key: string; value: Metadata; }>, ): MetadataObjectType => { const properties: MetadataProperty[] = definitions.map((def) => MetadataProperty.create({ key: MetadataFactory.soleLiteral(def.key), value: def.value, description: null, jsDocTags: [], }), ); const obj: MetadataObjectType = MetadataObjectType.create({ name: "object.o" + Math.random().toString().slice(2), properties, description: undefined, jsDocTags: [], index: -1, validated: true, recursive: false, nullables: [false], }); ProtobufFactory.emplaceObject(obj); return obj; };