typia
Version:
Superfast runtime validators with only one line
459 lines (446 loc) • 15 kB
text/typescript
import ts from "typescript";
import { ExpressionFactory } from "../factories/ExpressionFactory";
import { IdentifierFactory } from "../factories/IdentifierFactory";
import { StatementFactory } from "../factories/StatementFactory";
import { TypeFactory } from "../factories/TypeFactory";
import { IProgrammerProps } from "../transformers/IProgrammerProps";
import { ITypiaContext } from "../transformers/ITypiaContext";
import { CheckerProgrammer } from "./CheckerProgrammer";
import { FeatureProgrammer } from "./FeatureProgrammer";
import { IsProgrammer } from "./IsProgrammer";
import { FunctionProgrammer } from "./helpers/FunctionProgrammer";
import { IExpressionEntry } from "./helpers/IExpressionEntry";
import { OptionPredicator } from "./helpers/OptionPredicator";
import { check_everything } from "./internal/check_everything";
import { check_object } from "./internal/check_object";
export namespace ValidateProgrammer {
export interface IConfig {
equals: boolean;
standardSchema?: boolean;
}
export interface IProps extends IProgrammerProps {
config: IConfig;
}
export const decompose = (props: {
context: ITypiaContext;
modulo: ts.LeftHandSideExpression;
functor: FunctionProgrammer;
config: IConfig;
type: ts.Type;
name: string | undefined;
}): FeatureProgrammer.IDecomposed => {
const is: FeatureProgrammer.IDecomposed = IsProgrammer.decompose(props);
const composed: FeatureProgrammer.IComposed = CheckerProgrammer.compose({
...props,
config: {
prefix: "_v",
path: true,
trace: true,
numeric: OptionPredicator.numeric(props.context.options),
equals: props.config.equals,
atomist: (next) =>
[
...(next.entry.expression ? [next.entry.expression] : []),
...(next.entry.conditions.length === 0
? []
: next.entry.conditions.length === 1
? next.entry.conditions[0]!.map((cond) =>
ts.factory.createLogicalOr(
cond.expression,
create_report_call({
exceptionable:
next.explore.from === "top"
? ts.factory.createTrue()
: ts.factory.createIdentifier("_exceptionable"),
path: ts.factory.createIdentifier(
next.explore.postfix
? `_path + ${next.explore.postfix}`
: "_path",
),
expected: cond.expected,
value: next.input,
}),
),
)
: [
ts.factory.createLogicalOr(
next.entry.conditions
.map((set) =>
set
.map((s) => s.expression)
.reduce((a, b) =>
ts.factory.createLogicalAnd(a, b),
),
)
.reduce((a, b) => ts.factory.createLogicalOr(a, b)),
create_report_call({
exceptionable:
next.explore.from === "top"
? ts.factory.createTrue()
: ts.factory.createIdentifier("_exceptionable"),
path: ts.factory.createIdentifier(
next.explore.postfix
? `_path + ${next.explore.postfix}`
: "_path",
),
expected: next.entry.expected,
value: next.input,
}),
),
]),
].reduce((x, y) => ts.factory.createLogicalAnd(x, y)),
combiner: combine(props),
joiner: joiner(props),
success: ts.factory.createTrue(),
},
});
const arrow: ts.ArrowFunction = ts.factory.createArrowFunction(
undefined,
undefined,
[IdentifierFactory.parameter("input", TypeFactory.keyword("any"))],
props.context.importer.type({
file: "typia",
name: "IValidation",
arguments: [
ts.factory.createTypeReferenceNode(
props.name ??
TypeFactory.getFullName({
checker: props.context.checker,
type: props.type,
}),
),
],
}),
undefined,
ts.factory.createBlock(
[
// validate when false === is<T>(input)
ts.factory.createIfStatement(
ts.factory.createStrictEquality(
ts.factory.createFalse(),
ts.factory.createCallExpression(
ts.factory.createIdentifier("__is"),
undefined,
[ts.factory.createIdentifier("input")],
),
),
ts.factory.createBlock([
// prepare errors
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("errors"),
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
ts.factory.createArrayLiteralExpression([]),
),
),
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("_report"),
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
ts.factory.createCallExpression(
ts.factory.createAsExpression(
props.context.importer.internal("validateReport"),
TypeFactory.keyword("any"),
),
[],
[ts.factory.createIdentifier("errors")],
),
),
),
ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
ts.factory.createArrowFunction(
undefined,
undefined,
composed.parameters,
undefined,
undefined,
composed.body,
),
undefined,
[
ts.factory.createIdentifier("input"),
ts.factory.createStringLiteral("$input"),
ts.factory.createTrue(),
],
),
),
StatementFactory.constant({
name: "success",
value: ts.factory.createStrictEquality(
ExpressionFactory.number(0),
ts.factory.createIdentifier("errors.length"),
),
}),
ts.factory.createReturnStatement(
ts.factory.createAsExpression(
create_output(),
TypeFactory.keyword("any"),
),
),
]),
),
ts.factory.createReturnStatement(
ts.factory.createAsExpression(
ts.factory.createObjectLiteralExpression(
[
ts.factory.createPropertyAssignment(
"success",
ts.factory.createTrue(),
),
ts.factory.createPropertyAssignment(
"data",
ts.factory.createIdentifier("input"),
),
],
true,
),
TypeFactory.keyword("any"),
),
),
],
true,
),
);
return {
functions: {
...is.functions,
...composed.functions,
},
statements: [
...is.statements,
...composed.statements,
StatementFactory.constant({
name: "__is",
value: is.arrow,
}),
StatementFactory.mut({ name: "errors" }),
StatementFactory.mut({ name: "_report" }),
],
arrow,
};
};
export const write = (props: IProps) => {
const functor: FunctionProgrammer = new FunctionProgrammer(
props.modulo.getText(),
);
const result: FeatureProgrammer.IDecomposed = decompose({
config: props.config,
context: props.context,
modulo: props.modulo,
functor,
type: props.type,
name: props.name,
});
return FeatureProgrammer.writeDecomposed({
modulo: props.modulo,
functor,
result,
returnWrapper: props.config.standardSchema
? (arrow) =>
ts.factory.createCallExpression(
props.context.importer.internal("createStandardSchema"),
undefined,
[arrow],
)
: undefined,
});
};
}
const combine =
(props: {
config: ValidateProgrammer.IConfig;
context: ITypiaContext;
functor: FunctionProgrammer;
}): CheckerProgrammer.IConfig.Combiner =>
(next) => {
if (next.explore.tracable === false)
return IsProgrammer.configure({
options: {
object: (v) =>
validate_object({
context: props.context,
functor: props.functor,
config: props.config,
entries: v.entries,
input: v.input,
}),
numeric: true,
},
context: props.context,
functor: props.functor,
}).combiner(next);
const path: string = next.explore.postfix
? `_path + ${next.explore.postfix}`
: "_path";
return next.logic === "and"
? next.binaries
.map((binary) =>
binary.combined
? binary.expression
: ts.factory.createLogicalOr(
binary.expression,
create_report_call({
exceptionable:
next.explore.source === "top"
? ts.factory.createTrue()
: ts.factory.createIdentifier("_exceptionable"),
path: ts.factory.createIdentifier(path),
expected: next.expected,
value: next.input,
}),
),
)
.reduce(ts.factory.createLogicalAnd)
: ts.factory.createLogicalOr(
next.binaries
.map((binary) => binary.expression)
.reduce(ts.factory.createLogicalOr),
create_report_call({
exceptionable:
next.explore.source === "top"
? ts.factory.createTrue()
: ts.factory.createIdentifier("_exceptionable"),
path: ts.factory.createIdentifier(path),
expected: next.expected,
value: next.input,
}),
);
};
const validate_object = (props: {
config: ValidateProgrammer.IConfig;
context: ITypiaContext;
functor: FunctionProgrammer;
entries: IExpressionEntry<ts.Expression>[];
input: ts.Expression;
}) =>
check_object({
config: {
equals: props.config.equals,
undefined: true,
assert: false,
reduce: ts.factory.createLogicalAnd,
positive: ts.factory.createTrue(),
superfluous: (input, description) =>
create_report_call({
path: ts.factory.createAdd(
ts.factory.createIdentifier("_path"),
ts.factory.createCallExpression(
props.context.importer.internal("accessExpressionAsString"),
undefined,
[ts.factory.createIdentifier("key")],
),
),
expected: "undefined",
value: input,
description,
}),
halt: (expr) =>
ts.factory.createLogicalOr(
ts.factory.createStrictEquality(
ts.factory.createFalse(),
ts.factory.createIdentifier("_exceptionable"),
),
expr,
),
},
context: props.context,
entries: props.entries,
input: props.input,
});
const joiner = (props: {
config: ValidateProgrammer.IConfig;
context: ITypiaContext;
functor: FunctionProgrammer;
}): CheckerProgrammer.IConfig.IJoiner => ({
object: (v) =>
validate_object({
context: props.context,
functor: props.functor,
config: props.config,
entries: v.entries,
input: v.input,
}),
array: (props) =>
check_everything(
ts.factory.createCallExpression(
IdentifierFactory.access(props.input, "map"),
undefined,
[props.arrow],
),
),
failure: (next) =>
create_report_call({
exceptionable:
next.explore?.from === "top"
? ts.factory.createTrue()
: ts.factory.createIdentifier("_exceptionable"),
path: ts.factory.createIdentifier(
next.explore?.postfix ? `_path + ${next.explore.postfix}` : "_path",
),
expected: next.expected,
value: next.input,
}),
tuple: (binaries) =>
check_everything(ts.factory.createArrayLiteralExpression(binaries, true)),
});
const create_output = () =>
ts.factory.createConditionalExpression(
ts.factory.createIdentifier("success"),
undefined,
ts.factory.createObjectLiteralExpression(
[
ts.factory.createShorthandPropertyAssignment("success"),
ts.factory.createPropertyAssignment(
"data",
ts.factory.createIdentifier("input"),
),
],
true,
),
undefined,
ts.factory.createObjectLiteralExpression(
[
ts.factory.createShorthandPropertyAssignment("success"),
ts.factory.createShorthandPropertyAssignment("errors"),
ts.factory.createPropertyAssignment(
"data",
ts.factory.createIdentifier("input"),
),
],
true,
),
);
const create_report_call = (props: {
exceptionable?: ts.Expression;
path: ts.Expression;
expected: string;
value: ts.Expression;
description?: ts.Expression;
}): ts.Expression =>
ts.factory.createCallExpression(
ts.factory.createIdentifier("_report"),
undefined,
[
props.exceptionable ?? ts.factory.createIdentifier("_exceptionable"),
ts.factory.createObjectLiteralExpression(
[
ts.factory.createPropertyAssignment("path", props.path),
ts.factory.createPropertyAssignment(
"expected",
ts.factory.createStringLiteral(props.expected),
),
ts.factory.createPropertyAssignment("value", props.value),
...(props.description
? [
ts.factory.createPropertyAssignment(
"description",
props.description,
),
]
: []),
],
true,
),
],
);