typia
Version:
Superfast runtime validators with only one line
647 lines (624 loc) • 21.9 kB
text/typescript
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 { MetadataArray } from "../../schemas/metadata/MetadataArray";
import { MetadataAtomic } from "../../schemas/metadata/MetadataAtomic";
import { MetadataObject } from "../../schemas/metadata/MetadataObject";
import { MetadataProperty } from "../../schemas/metadata/MetadataProperty";
import { IProject } from "../../transformers/IProject";
import { ProtobufAtomic } from "../../typings/ProtobufAtomic";
import { FeatureProgrammer } from "../FeatureProgrammer";
import { FunctionImporter } from "../helpers/FunctionImporter";
import { ProtobufUtil } from "../helpers/ProtobufUtil";
export namespace ProtobufDecodeProgrammer {
export const decompose = (props: {
project: IProject;
modulo: ts.LeftHandSideExpression;
importer: FunctionImporter;
type: ts.Type;
name: string | undefined;
}): FeatureProgrammer.IDecomposed => {
const collection: MetadataCollection = new MetadataCollection();
const meta: Metadata = ProtobufFactory.metadata(props.modulo.getText())(
props.project.checker,
props.project.context,
)(collection)(props.type);
return {
functions: Object.fromEntries(
collection
.objects()
.filter((obj) => ProtobufUtil.isStaticObject(obj))
.map((obj) => [
`${PREFIX}o${obj.index}`,
StatementFactory.constant(
props.importer.useLocal(`${PREFIX}o${obj.index}`),
write_object_function(props.project)(props.importer)(obj),
),
]),
),
statements: [],
arrow: ts.factory.createArrowFunction(
undefined,
undefined,
[
IdentifierFactory.parameter(
"input",
ts.factory.createTypeReferenceNode("Uint8Array"),
),
],
ts.factory.createImportTypeNode(
ts.factory.createLiteralTypeNode(
ts.factory.createStringLiteral("typia"),
),
undefined,
ts.factory.createIdentifier("Resolved"),
[
ts.factory.createTypeReferenceNode(
props.name ??
TypeFactory.getFullName(props.project.checker)(props.type),
),
],
),
undefined,
ts.factory.createBlock(
[
StatementFactory.constant(
"reader",
ts.factory.createNewExpression(
props.importer.use("Reader"),
undefined,
[ts.factory.createIdentifier("input")],
),
),
ts.factory.createReturnStatement(
decode_regular_object(true)(meta.objects[0]!),
),
],
true,
),
),
};
};
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({
project,
modulo,
importer,
type,
name,
});
return FeatureProgrammer.writeDecomposed({
modulo,
importer,
result,
});
};
const write_object_function =
(project: IProject) =>
(importer: FunctionImporter) =>
(obj: MetadataObject): 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,
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("size"),
undefined,
undefined,
),
undefined,
ts.factory.createAdd(
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("index"),
undefined,
undefined,
),
ts.factory.createIdentifier("length"),
),
),
),
),
...write_object_function_body(project)(importer)({
condition: ts.factory.createLessThan(
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("index"),
undefined,
undefined,
),
ts.factory.createIdentifier("length"),
),
tag: "tag",
output: "output",
})(obj.properties),
ts.factory.createReturnStatement(
ts.factory.createIdentifier("output"),
),
],
true,
),
);
const write_object_function_body =
(project: IProject) =>
(importer: FunctionImporter) =>
(props: { condition: ts.Expression; tag: string; output: string }) =>
(properties: MetadataProperty[]): ts.Statement[] => {
let i: number = 1;
const clauses: ts.CaseClause[] = properties
.map((p) => {
const clause = decode_property(project)(importer)(i)(
IdentifierFactory.access(ts.factory.createIdentifier(props.output))(
p.key.getSoleLiteral()!,
),
p.value,
);
i += ProtobufUtil.size(p.value);
return clause;
})
.flat();
return [
StatementFactory.constant(
props.output,
ts.factory.createAsExpression(
ts.factory.createObjectLiteralExpression(
properties
.filter(
(p) =>
!(
project.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(
props.tag,
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("uint32"),
undefined,
undefined,
),
),
ts.factory.createSwitchStatement(
ts.factory.createUnsignedRightShift(
ts.factory.createIdentifier(props.tag),
ExpressionFactory.number(3),
),
ts.factory.createCaseBlock([
...clauses,
ts.factory.createDefaultClause([
ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("skipType"),
undefined,
[
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,
[],
)
: 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),
)
? ts.factory.createObjectLiteralExpression()
: ts.factory.createIdentifier("undefined"),
TypeFactory.keyword("any"),
);
/* -----------------------------------------------------------
DECODERS
----------------------------------------------------------- */
const decode_property =
(project: IProject) =>
(importer: FunctionImporter) =>
(index: number) =>
(
accessor: ts.ElementAccessExpression | ts.PropertyAccessExpression,
meta: Metadata,
): ts.CaseClause[] => {
const clauses: ts.CaseClause[] = [];
const emplace = (name: string) => (v: ts.Expression | ts.Statement[]) =>
clauses.push(
ts.factory.createCaseClause(
ExpressionFactory.number(index++),
Array.isArray(v)
? [
ts.factory.createExpressionStatement(
ts.factory.createIdentifier(`// type: ${name}`),
),
...v,
ts.factory.createBreakStatement(),
]
: [
ts.factory.createExpressionStatement(
ts.factory.createIdentifier(`// ${name}`),
),
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
accessor,
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
v,
),
),
ts.factory.createBreakStatement(),
],
),
);
const required: boolean = meta.isRequired() && !meta.nullable;
for (const atomic of ProtobufUtil.getAtomics(meta))
emplace(atomic)(decode_atomic(meta)(atomic));
if (meta.natives.length) emplace("bytes")(decode_bytes("bytes"));
for (const array of meta.arrays)
emplace(`Array<${array.type.value.getName()}>`)(
decode_array(accessor, array, required),
);
for (const map of meta.maps)
emplace(`Map<string, ${map.value.getName()}>`)(
decode_map(project)(importer)(accessor, map, required),
);
for (const obj of meta.objects)
emplace(obj.name)(
ProtobufUtil.isStaticObject(obj)
? decode_regular_object(false)(obj)
: decode_dynamic_object(project)(importer)(accessor, obj, required),
);
return clauses;
};
const decode_atomic =
(meta: Metadata) =>
(atomic: ProtobufAtomic): ts.Expression => {
if (atomic === "string") return decode_bytes("string");
const call: ts.CallExpression = ts.factory.createCallExpression(
IdentifierFactory.access(ts.factory.createIdentifier("reader"))(atomic),
undefined,
undefined,
);
if (atomic !== "int64" && atomic !== "uint64") return call;
const isNumber: boolean = ProtobufUtil.getNumbers(meta).some(
(n) => n === atomic,
);
return isNumber
? ts.factory.createCallExpression(
ts.factory.createIdentifier("Number"),
undefined,
[call],
)
: call;
};
const decode_bytes = (method: "bytes" | "string"): ts.Expression =>
ts.factory.createCallExpression(
IdentifierFactory.access(ts.factory.createIdentifier("reader"))(method),
undefined,
undefined,
);
const decode_array = (
accessor: ts.ElementAccessExpression | ts.PropertyAccessExpression,
array: MetadataArray,
required: boolean,
): ts.Statement[] => {
const statements: Array<ts.Expression | ts.Statement> = [];
if (required === false)
statements.push(
ts.factory.createBinaryExpression(
accessor,
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionEqualsToken),
ts.factory.createAsExpression(
ts.factory.createArrayLiteralExpression(),
ts.factory.createTypeReferenceNode("any[]"),
),
),
);
const atomics = ProtobufUtil.getAtomics(array.type.value);
const decoder = atomics.length
? () => decode_atomic(array.type.value)(atomics[0]!)
: array.type.value.natives.length
? () => decode_bytes("bytes")
: array.type.value.objects.length
? () => decode_regular_object(false)(array.type.value.objects[0]!)
: null;
if (decoder === null) throw new Error("Never reach here.");
else if (atomics.length && atomics[0] !== "string") {
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(
"piece",
ts.factory.createAdd(
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("uint32"),
undefined,
undefined,
),
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("index"),
undefined,
undefined,
),
),
),
ts.factory.createWhileStatement(
ts.factory.createLessThan(
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("index"),
undefined,
undefined,
),
ts.factory.createIdentifier("piece"),
),
ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
IdentifierFactory.access(accessor)("push"),
undefined,
[decoder()],
),
),
),
],
true,
),
ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
IdentifierFactory.access(accessor)("push"),
undefined,
[decoder()],
),
),
),
);
} else
statements.push(
ts.factory.createCallExpression(
IdentifierFactory.access(accessor)("push"),
undefined,
[decoder()],
),
);
return statements.map((stmt) =>
ts.isExpression(stmt) ? ts.factory.createExpressionStatement(stmt) : stmt,
);
};
const decode_regular_object =
(top: boolean) =>
(obj: MetadataObject): ts.Expression =>
ts.factory.createCallExpression(
ts.factory.createIdentifier(`${PREFIX}o${obj.index}`),
undefined,
[
ts.factory.createIdentifier("reader"),
...(top
? []
: [
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("uint32"),
undefined,
undefined,
),
]),
],
);
const decode_dynamic_object =
(project: IProject) =>
(importer: FunctionImporter) =>
(
accessor: ts.ElementAccessExpression | ts.PropertyAccessExpression,
obj: MetadataObject,
required: boolean,
): ts.Statement[] => {
const top = obj.properties[0]!;
return decode_entry(project)(importer)({
initializer: () =>
ts.factory.createBinaryExpression(
accessor,
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionEqualsToken),
ts.factory.createObjectLiteralExpression(),
),
setter: () =>
ts.factory.createBinaryExpression(
ts.factory.createElementAccessExpression(
accessor,
ts.factory.createIdentifier("entry.key"),
),
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
ts.factory.createIdentifier("entry.value"),
),
})(
MetadataProperty.create({
...top,
key: (() => {
const key: Metadata = Metadata.initialize();
key.atomics.push(
MetadataAtomic.create({
type: "string",
tags: [],
}),
);
return key;
})(),
}),
required,
);
};
const decode_map =
(project: IProject) =>
(importer: FunctionImporter) =>
(
accessor: ts.ElementAccessExpression | ts.PropertyAccessExpression,
map: Metadata.Entry,
required: boolean,
): ts.Statement[] =>
decode_entry(project)(importer)({
initializer: () =>
ts.factory.createBinaryExpression(
accessor,
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionEqualsToken),
ts.factory.createNewExpression(
ts.factory.createIdentifier("Map"),
[TypeFactory.keyword("any"), TypeFactory.keyword("any")],
[],
),
),
setter: () =>
ts.factory.createCallExpression(
IdentifierFactory.access(accessor)("set"),
undefined,
[
ts.factory.createIdentifier("entry.key"),
ts.factory.createIdentifier("entry.value"),
],
),
})(map, required);
const decode_entry =
(project: IProject) =>
(importer: FunctionImporter) =>
(props: {
initializer: () => ts.Expression;
setter: () => ts.Expression;
}) =>
(map: Metadata.Entry, required: boolean): ts.Statement[] => {
const statements: ts.Statement[] = [
...(required
? []
: [ts.factory.createExpressionStatement(props.initializer())]),
StatementFactory.constant(
"piece",
ts.factory.createAdd(
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("uint32"),
undefined,
undefined,
),
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("index"),
undefined,
undefined,
),
),
),
...write_object_function_body(project)(importer)({
condition: ts.factory.createLessThan(
ts.factory.createCallExpression(
IdentifierFactory.access(READER())("index"),
undefined,
undefined,
),
ts.factory.createIdentifier("piece"),
),
tag: "kind",
output: "entry",
})([
MetadataProperty.create({
key: MetadataFactory.soleLiteral("key"),
value: map.key,
description: null,
jsDocTags: [],
}),
MetadataProperty.create({
key: MetadataFactory.soleLiteral("value"),
value: map.value,
description: null,
jsDocTags: [],
}),
]),
ts.factory.createExpressionStatement(props.setter()),
];
return [
ts.factory.createExpressionStatement(
ExpressionFactory.selfCall(ts.factory.createBlock(statements, true)),
),
];
};
}
const PREFIX = "$pd";
const READER = () => ts.factory.createIdentifier("reader");