typia
Version:
Superfast runtime validators with only one line
309 lines (292 loc) • 10.2 kB
text/typescript
import ts from "typescript";
import { IdentifierFactory } from "../../factories/IdentifierFactory";
import { MetadataCollection } from "../../factories/MetadataCollection";
import { MetadataFactory } from "../../factories/MetadataFactory";
import { StatementFactory } from "../../factories/StatementFactory";
import { TypeFactory } from "../../factories/TypeFactory";
import { Metadata } from "../../schemas/metadata/Metadata";
import { MetadataArrayType } from "../../schemas/metadata/MetadataArrayType";
import { MetadataObjectType } from "../../schemas/metadata/MetadataObjectType";
import { MetadataProperty } from "../../schemas/metadata/MetadataProperty";
import { IProgrammerProps } from "../../transformers/IProgrammerProps";
import { ITypiaContext } from "../../transformers/ITypiaContext";
import { TransformerError } from "../../transformers/TransformerError";
import { Atomic } from "../../typings/Atomic";
import { Escaper } from "../../utils/Escaper";
import { StringUtil } from "../../utils/StringUtil";
import { FeatureProgrammer } from "../FeatureProgrammer";
import { FunctionProgrammer } from "../helpers/FunctionProgrammer";
import { HttpMetadataUtil } from "../helpers/HttpMetadataUtil";
export namespace HttpFormDataProgrammer {
export const decompose = (props: {
context: ITypiaContext;
functor: FunctionProgrammer;
type: ts.Type;
name: string | undefined;
}): FeatureProgrammer.IDecomposed => {
// ANALYZE TYPE
const collection: MetadataCollection = new MetadataCollection();
const result = MetadataFactory.analyze({
checker: props.context.checker,
transformer: props.context.transformer,
options: {
escape: false,
constant: true,
absorb: true,
validate,
},
collection,
type: props.type,
});
if (result.success === false)
throw TransformerError.from({
code: props.functor.method,
errors: result.errors,
});
// DO TRANSFORM
const object: MetadataObjectType = result.data.objects[0]!.type;
const statements: ts.Statement[] = decode_object({
context: props.context,
object,
});
return {
functions: {},
statements: [],
arrow: ts.factory.createArrowFunction(
undefined,
undefined,
[
IdentifierFactory.parameter(
"input",
ts.factory.createTypeReferenceNode("FormData"),
),
],
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(statements, 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,
});
};
export const validate = (
meta: Metadata,
explore: MetadataFactory.IExplore,
): string[] => {
const errors: string[] = [];
const insert = (msg: string) => errors.push(msg);
if (explore.top === true) {
// TOP MUST BE ONLY OBJECT
if (meta.objects.length !== 1 || meta.bucket() !== 1)
insert("only one object type is allowed.");
if (meta.nullable === true) insert("formdata parameters cannot be null.");
if (meta.isRequired() === false)
insert("formdata parameters cannot be undefined.");
} else if (
explore.nested !== null &&
explore.nested instanceof MetadataArrayType
) {
//----
// ARRAY
//----
const atomics = HttpMetadataUtil.atomics(meta);
const expected: number =
meta.atomics.length +
meta.templates.length +
meta.constants.map((c) => c.values.length).reduce((a, b) => a + b, 0) +
meta.natives.filter(
(native) => native.name === "Blob" || native.name === "File",
).length;
if (atomics.size > 1) insert("union type is not allowed in array.");
if (meta.size() !== expected)
insert(
"only atomic, constant or blob (file) types are allowed in array.",
);
} else if (explore.object && explore.property !== null) {
//----
// COMMON
//----
// PROPERTY MUST BE SOLE
if (typeof explore.property === "object")
insert("dynamic property is not allowed.");
// DO NOT ALLOW TUPLE TYPE
if (meta.tuples.length) insert("tuple type is not allowed.");
// DO NOT ALLOW UNION TYPE
if (HttpMetadataUtil.isUnion(meta)) insert("union type is not allowed.");
// DO NOT ALLOW NESTED OBJECT
if (
meta.objects.length ||
meta.sets.length ||
meta.maps.length ||
meta.natives.filter(
(native) => native.name !== "Blob" && native.name !== "File",
).length
)
insert("nested object type is not allowed.");
}
return errors;
};
const decode_object = (props: {
context: ITypiaContext;
object: MetadataObjectType;
}): ts.Statement[] => {
// const input: ts.Identifier = ts.factory.createIdentifier("input");
const output: ts.Identifier = ts.factory.createIdentifier("output");
return [
StatementFactory.constant({
name: "output",
value: ts.factory.createObjectLiteralExpression(
props.object.properties.map((p) =>
decode_regular_property({
context: props.context,
property: p,
}),
),
true,
),
}),
ts.factory.createReturnStatement(
ts.factory.createAsExpression(output, TypeFactory.keyword("any")),
),
];
};
const decode_regular_property = (props: {
context: ITypiaContext;
property: MetadataProperty;
}): ts.PropertyAssignment => {
const key: string = props.property.key.constants[0]!.values[0]!
.value as string;
const value: Metadata = props.property.value;
const [type, isArray]: [Atomic.Literal | "blob" | "file", boolean] = value
.atomics.length
? [value.atomics[0]!.type, false]
: value.constants.length
? [value.constants[0]!.type, false]
: value.templates.length
? ["string", false]
: value.natives.some((native) => native.name === "Blob")
? ["blob", false]
: value.natives.some((native) => native.name === "File")
? ["file", false]
: (() => {
const meta =
value.arrays[0]?.type.value ??
value.tuples[0]!.type.elements[0]!;
return meta.atomics.length
? [meta.atomics[0]!.type, true]
: meta.templates.length
? ["string", true]
: meta.natives.some((native) => native.name === "Blob")
? ["blob", true]
: meta.natives.some((native) => native.name === "File")
? ["file", true]
: [meta.constants[0]!.type, true];
})();
return ts.factory.createPropertyAssignment(
Escaper.variable(key) ? key : ts.factory.createStringLiteral(key),
isArray
? decode_array({
context: props.context,
metadata: value,
input: ts.factory.createCallExpression(
IdentifierFactory.access(
ts.factory.createCallExpression(
ts.factory.createIdentifier("input.getAll"),
undefined,
[ts.factory.createStringLiteral(key)],
),
"map",
),
undefined,
[
ts.factory.createArrowFunction(
undefined,
undefined,
[IdentifierFactory.parameter("elem")],
undefined,
undefined,
decode_value({
context: props.context,
type,
coalesce: false,
input: ts.factory.createIdentifier("elem"),
}),
),
],
),
})
: decode_value({
context: props.context,
type,
coalesce: value.nullable === false && value.isRequired() === false,
input: ts.factory.createCallExpression(
ts.factory.createIdentifier("input.get"),
undefined,
[ts.factory.createStringLiteral(key)],
),
}),
);
};
const decode_value = (props: {
context: ITypiaContext;
type: Atomic.Literal | "blob" | "file";
coalesce: boolean;
input: ts.Expression;
}) => {
const call = ts.factory.createCallExpression(
props.context.importer.internal(
`httpFormDataRead${StringUtil.capitalize(props.type)}`,
),
undefined,
[props.input],
);
return props.coalesce
? ts.factory.createBinaryExpression(
call,
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("undefined"),
)
: call;
};
const decode_array = (props: {
context: ITypiaContext;
metadata: Metadata;
input: ts.Expression;
}): ts.Expression =>
props.metadata.nullable || props.metadata.isRequired() === false
? ts.factory.createCallExpression(
props.context.importer.internal("httpFormDataReadArray"),
undefined,
[
props.input,
props.metadata.nullable
? ts.factory.createNull()
: ts.factory.createIdentifier("undefined"),
],
)
: props.input;
}