typia
Version:
Superfast runtime validators with only one line
606 lines (567 loc) • 18.8 kB
text/typescript
import ts from "typescript";
import { IdentifierFactory } from "../factories/IdentifierFactory";
import { MetadataCollection } from "../factories/MetadataCollection";
import { StatementFactory } from "../factories/StatementFactory";
import { TypeFactory } from "../factories/TypeFactory";
import { ValueFactory } from "../factories/ValueFactory";
import { Metadata } from "../schemas/metadata/Metadata";
import { MetadataArray } from "../schemas/metadata/MetadataArray";
import { MetadataObjectType } from "../schemas/metadata/MetadataObjectType";
import { ITypiaContext } from "../transformers/ITypiaContext";
import { CheckerProgrammer } from "./CheckerProgrammer";
import { FunctionProgrammer } from "./helpers/FunctionProgrammer";
import { IExpressionEntry } from "./helpers/IExpressionEntry";
import { UnionExplorer } from "./helpers/UnionExplorer";
import { feature_object_entries } from "./internal/feature_object_entries";
export namespace FeatureProgrammer {
/* -----------------------------------------------------------
PARAMETERS
----------------------------------------------------------- */
export interface IConfig<Output extends ts.ConciseBody = ts.ConciseBody> {
types: IConfig.ITypes;
/** Prefix name of internal functions for specific types. */
prefix: string;
/** Whether to archive access path or not. */
path: boolean;
/** Whether to trace exception or not. */
trace: boolean;
addition?: undefined | ((collection: MetadataCollection) => ts.Statement[]);
/** Initializer of metadata. */
initializer: (props: {
context: ITypiaContext;
functor: FunctionProgrammer;
type: ts.Type;
}) => {
collection: MetadataCollection;
metadata: Metadata;
};
/** Decoder, station of every types. */
decoder: (props: {
metadata: Metadata;
input: ts.Expression;
explore: IExplore;
}) => Output;
/** Object configurator. */
objector: IConfig.IObjector<Output>;
/** Generator of functions for object types. */
generator: IConfig.IGenerator;
}
export namespace IConfig {
export interface ITypes {
input: (type: ts.Type, name?: undefined | string) => ts.TypeNode;
output: (type: ts.Type, name?: undefined | string) => ts.TypeNode;
}
export interface IObjector<Output extends ts.ConciseBody = ts.ConciseBody> {
/** Type checker when union object type comes. */
checker: (props: {
metadata: Metadata;
input: ts.Expression;
explore: IExplore;
}) => ts.Expression;
/** Decoder, function call expression generator of specific typed objects. */
decoder: (props: {
input: ts.Expression;
object: MetadataObjectType;
explore: IExplore;
}) => ts.Expression;
/** Joiner of expressions from properties. */
joiner(props: {
entries: IExpressionEntry<Output>[];
input?: ts.Expression;
object?: MetadataObjectType;
}): ts.ConciseBody;
/**
* Union type specificator.
*
* Expression of an algorithm specifying object type and calling the
* `decoder` function of the specified object type.
*/
unionizer: (props: {
objects: MetadataObjectType[];
input: ts.Expression;
explore: IExplore;
}) => ts.Expression;
/**
* Handler of union type specification failure.
*
* @param props Properties of failure
* @returns Statement of failure
*/
failure(props: {
input: ts.Expression;
expected: string;
explore?: undefined | IExplore;
}): ts.Statement;
/**
* Transformer of type checking expression by discrimination.
*
* When an object type has been specified by a discrimination without full
* iteration, the `unionizer` will decode the object instance after the
* last type checking.
*
* In such circumtance, you can transform the last type checking function.
*
* @deprecated
* @param exp Current expression about type checking
* @returns Transformed expression
*/
is?: undefined | ((exp: ts.Expression) => ts.Expression);
/**
* Transformer of non-undefined type checking by discrimination.
*
* When specifying an union type of objects, `typia` tries to find
* discrimination way just by checking only one property type. If
* succeeded to find the discrimination way, `typia` will check the target
* property type and in the checking, non-undefined type checking would be
* done.
*
* In such process, you can transform the non-undefined type checking.
*
* @deprecated
* @param exp
* @returns Transformed expression
*/
required?: undefined | ((exp: ts.Expression) => ts.Expression);
/**
* Condition wrapper when unable to specify any object type.
*
* When failed to specify an object type through discrimination, full
* iteration type checking would be happened. In such circumstance, you
* can wrap the condition with additional function.
*
* @param props Properties of condition
* @returns The wrapper expression
*/
full?:
| undefined
| ((props: {
condition: ts.Expression;
input: ts.Expression;
expected: string;
explore: IExplore;
}) => ts.Expression);
/** Return type. */
type?: undefined | ts.TypeNode;
}
export interface IGenerator {
objects?:
| undefined
| ((collection: MetadataCollection) => ts.VariableStatement[]);
unions?:
| undefined
| ((collection: MetadataCollection) => ts.VariableStatement[]);
arrays: (collection: MetadataCollection) => ts.VariableStatement[];
tuples: (collection: MetadataCollection) => ts.VariableStatement[];
}
}
export interface IExplore {
tracable: boolean;
source: "top" | "function";
from: "top" | "array" | "object";
postfix: string;
start?: undefined | number;
}
export type Decoder<
T,
Output extends ts.ConciseBody = ts.ConciseBody,
> = (props: {
input: ts.Expression;
definition: T;
explore: IExplore;
}) => Output;
/* -----------------------------------------------------------
GENERATORS
----------------------------------------------------------- */
export interface IComposed {
body: ts.ConciseBody;
parameters: ts.ParameterDeclaration[];
functions: Record<string, ts.VariableStatement>;
statements: ts.Statement[];
response: ts.TypeNode;
}
export interface IDecomposed {
functions: Record<string, ts.VariableStatement>;
statements: ts.Statement[];
arrow: ts.ArrowFunction;
}
export const compose = (props: {
context: ITypiaContext;
config: IConfig;
functor: FunctionProgrammer;
type: ts.Type;
name: string | undefined;
}): IComposed => {
const { collection, metadata } = props.config.initializer(props);
return {
body: props.config.decoder({
input: ValueFactory.INPUT(),
metadata,
explore: {
tracable: props.config.path || props.config.trace,
source: "top",
from: "top",
postfix: '""',
},
}),
statements: props.config.addition
? props.config.addition(collection)
: [],
functions: {
...Object.fromEntries(
(
props.config.generator.objects?.(collection) ??
write_object_functions({
...props,
collection,
})
).map((v, i) => [`${props.config.prefix}o${i}`, v]),
),
...Object.fromEntries(
(
props.config.generator.unions?.(collection) ??
write_union_functions({
config: props.config,
collection,
})
).map((v, i) => [`${props.config.prefix}u${i}`, v]),
),
...Object.fromEntries(
props.config.generator
.arrays(collection)
.map((v, i) => [`${props.config.prefix}a${i}`, v]),
),
...Object.fromEntries(
props.config.generator
.tuples(collection)
.map((v, i) => [`${props.config.prefix}t${i}`, v]),
),
},
parameters: parameterDeclarations({
config: props.config,
type: props.config.types.input(props.type, props.name),
input: ValueFactory.INPUT(),
}),
response: props.config.types.output(props.type, props.name),
};
};
export const writeDecomposed = (props: {
modulo: ts.LeftHandSideExpression;
functor: FunctionProgrammer;
result: IDecomposed;
returnWrapper?: (arrow: ts.ArrowFunction) => ts.Expression;
}): ts.CallExpression =>
ts.factory.createCallExpression(
ts.factory.createArrowFunction(
undefined,
undefined,
[],
undefined,
undefined,
ts.factory.createBlock([
...props.functor.declare(),
...Object.entries(props.result.functions)
.filter(([k]) => props.functor.hasLocal(k))
.map(([_k, v]) => v),
...props.result.statements,
ts.factory.createReturnStatement(
props.returnWrapper
? props.returnWrapper(props.result.arrow)
: props.result.arrow,
),
]),
),
undefined,
undefined,
);
export const write = (props: {
context: ITypiaContext;
config: IConfig;
functor: FunctionProgrammer;
type: ts.Type;
name?: string | undefined;
}): ts.ArrowFunction => {
// ITERATE OVER ALL METADATA
const { collection, metadata } = props.config.initializer(props);
const output: ts.ConciseBody = props.config.decoder({
metadata,
input: ValueFactory.INPUT(),
explore: {
tracable: props.config.path || props.config.trace,
source: "top",
from: "top",
postfix: '""',
},
});
// RETURNS THE OPTIMAL ARROW FUNCTION
const functions = {
objects:
props.config.generator.objects?.(collection) ??
write_object_functions({
config: props.config,
context: props.context,
collection,
}),
unions:
props.config.generator.unions?.(collection) ??
write_union_functions({
config: props.config,
collection,
}),
arrays: props.config.generator.arrays(collection),
tuples: props.config.generator.tuples(collection),
};
const added: ts.Statement[] = (props.config.addition ?? (() => []))(
collection,
);
return ts.factory.createArrowFunction(
undefined,
undefined,
parameterDeclarations({
config: props.config,
type: props.config.types.input(props.type, props.name),
input: ValueFactory.INPUT(),
}),
props.config.types.output(props.type, props.name),
undefined,
ts.factory.createBlock(
[
...added,
...functions.objects.filter((_, i) =>
props.functor.hasLocal(`${props.config.prefix}o${i}`),
),
...functions.unions.filter((_, i) =>
props.functor.hasLocal(`${props.config.prefix}u${i}`),
),
...functions.arrays.filter((_, i) =>
props.functor.hasLocal(`${props.config.prefix}a${i}`),
),
...functions.tuples.filter((_, i) =>
props.functor.hasLocal(`${props.config.prefix}t${i}`),
),
...(ts.isBlock(output)
? output.statements
: [ts.factory.createReturnStatement(output)]),
],
true,
),
);
};
export const write_object_functions = (props: {
config: IConfig;
context: ITypiaContext;
collection: MetadataCollection;
}) =>
props.collection.objects().map((object) =>
StatementFactory.constant({
name: `${props.config.prefix}o${object.index}`,
value: ts.factory.createArrowFunction(
undefined,
undefined,
parameterDeclarations({
config: props.config,
type: TypeFactory.keyword("any"),
input: ValueFactory.INPUT(),
}),
props.config.objector.type ?? TypeFactory.keyword("any"),
undefined,
props.config.objector.joiner({
input: ts.factory.createIdentifier("input"),
entries: feature_object_entries({
config: props.config,
context: props.context,
input: ts.factory.createIdentifier("input"),
object,
}),
object,
}),
),
}),
);
export const write_union_functions = (props: {
config: IConfig;
collection: MetadataCollection;
}) =>
props.collection.unions().map((union, i) =>
StatementFactory.constant({
name: `${props.config.prefix}u${i}`,
value: write_union({
config: props.config,
objects: union,
}),
}),
);
const write_union = (props: {
config: IConfig;
objects: MetadataObjectType[];
}) =>
ts.factory.createArrowFunction(
undefined,
undefined,
parameterDeclarations({
config: props.config,
type: TypeFactory.keyword("any"),
input: ValueFactory.INPUT(),
}),
TypeFactory.keyword("any"),
undefined,
UnionExplorer.object({
config: props.config,
objects: props.objects,
input: ValueFactory.INPUT(),
explore: {
tracable: props.config.path || props.config.trace,
source: "function",
from: "object",
postfix: "",
},
}),
);
/* -----------------------------------------------------------
DECODERS
----------------------------------------------------------- */
export const decode_array = (props: {
config: Pick<IConfig, "trace" | "path" | "decoder" | "prefix">;
functor: FunctionProgrammer;
combiner: (next: {
input: ts.Expression;
arrow: ts.ArrowFunction;
}) => ts.Expression;
array: MetadataArray;
input: ts.Expression;
explore: IExplore;
}) => {
const rand: string = props.functor.increment().toString();
const tail =
props.config.path || props.config.trace
? [
IdentifierFactory.parameter(
"_index" + rand,
TypeFactory.keyword("number"),
),
]
: [];
const arrow: ts.ArrowFunction = ts.factory.createArrowFunction(
undefined,
undefined,
[
IdentifierFactory.parameter("elem", TypeFactory.keyword("any")),
...tail,
],
undefined,
undefined,
props.config.decoder({
input: ValueFactory.INPUT("elem"),
metadata: props.array.type.value,
explore: {
tracable: props.explore.tracable,
source: props.explore.source,
from: "array",
postfix: index({
start: props.explore.start ?? null,
postfix: props.explore.postfix,
rand,
}),
},
}),
);
return props.combiner({
input: props.input,
arrow,
});
};
export const decode_object = (props: {
config: Pick<IConfig, "trace" | "path" | "prefix">;
functor: FunctionProgrammer;
object: MetadataObjectType;
input: ts.Expression;
explore: IExplore;
}) =>
ts.factory.createCallExpression(
ts.factory.createIdentifier(
props.functor.useLocal(`${props.config.prefix}o${props.object.index}`),
),
undefined,
argumentsArray(props),
);
/* -----------------------------------------------------------
UTILITIES FOR INTERNAL FUNCTIONS
----------------------------------------------------------- */
export const index = (props: {
start: number | null;
postfix: string;
rand: string;
}) => {
const tail: string =
props.start !== null
? `"[" + (${props.start} + _index${props.rand}) + "]"`
: `"[" + _index${props.rand} + "]"`;
if (props.postfix === "") return tail;
else if (props.postfix[props.postfix.length - 1] === `"`)
return (
props.postfix.substring(0, props.postfix.length - 1) + tail.substring(1)
);
return props.postfix + ` + ${tail}`;
};
export const argumentsArray = (props: {
config: Pick<IConfig, "path" | "trace">;
input: ts.Expression;
explore: FeatureProgrammer.IExplore;
}) => {
const tail: ts.Expression[] =
props.config.path === false && props.config.trace === false
? []
: props.config.path === true && props.config.trace === true
? [
ts.factory.createIdentifier(
props.explore.postfix
? `_path + ${props.explore.postfix}`
: "_path",
),
props.explore.source === "function"
? ts.factory.createIdentifier(
`${props.explore.tracable} && _exceptionable`,
)
: props.explore.tracable
? ts.factory.createTrue()
: ts.factory.createFalse(),
]
: props.config.path === true
? [
ts.factory.createIdentifier(
props.explore.postfix
? `_path + ${props.explore.postfix}`
: "_path",
),
]
: [
props.explore.source === "function"
? ts.factory.createIdentifier(
`${props.explore.tracable} && _exceptionable`,
)
: props.explore.tracable
? ts.factory.createTrue()
: ts.factory.createFalse(),
];
return [props.input, ...tail];
};
export const parameterDeclarations = (props: {
config: Pick<CheckerProgrammer.IConfig, "path" | "trace">;
type: ts.TypeNode;
input: ts.Identifier;
}) => {
const tail: ts.ParameterDeclaration[] = [];
if (props.config.path)
tail.push(
IdentifierFactory.parameter("_path", TypeFactory.keyword("string")),
);
if (props.config.trace)
tail.push(
IdentifierFactory.parameter(
"_exceptionable",
TypeFactory.keyword("boolean"),
ts.factory.createTrue(),
),
);
return [IdentifierFactory.parameter(props.input, props.type), ...tail];
};
}