typia
Version:
Superfast runtime validators with only one line
405 lines (379 loc) • 10.4 kB
text/typescript
import ts from "typescript";
import { Metadata } from "../schemas/metadata/Metadata";
import { MetadataAliasType } from "../schemas/metadata/MetadataAliasType";
import { MetadataArrayType } from "../schemas/metadata/MetadataArrayType";
import { MetadataConstant } from "../schemas/metadata/MetadataConstant";
import { MetadataFunction } from "../schemas/metadata/MetadataFunction";
import { MetadataObject } from "../schemas/metadata/MetadataObject";
import { MetadataObjectType } from "../schemas/metadata/MetadataObjectType";
import { MetadataTupleType } from "../schemas/metadata/MetadataTupleType";
import { explore_metadata } from "./internal/metadata/explore_metadata";
import { iterate_metadata_collection } from "./internal/metadata/iterate_metadata_collection";
import { iterate_metadata_sort } from "./internal/metadata/iterate_metadata_sort";
import { ValidationPipe } from "../typings/ValidationPipe";
import { ExpressionFactory } from "./ExpressionFactory";
import { MetadataCollection } from "./MetadataCollection";
export namespace MetadataFactory {
export type Validator = (meta: Metadata, explore: IExplore) => string[];
export interface IProps {
checker: ts.TypeChecker;
transformer: ts.TransformationContext | undefined;
options: IOptions;
collection: MetadataCollection;
type: ts.Type | null;
}
export interface IOptions {
escape: boolean;
constant: boolean;
absorb: boolean;
functional?: boolean;
validate?: Validator;
onError?: (node: ts.Node | undefined, message: string) => void;
}
export interface IExplore {
top: boolean;
object: MetadataObjectType | null;
property: string | object | null;
nested: null | MetadataAliasType | MetadataArrayType | MetadataTupleType;
parameter: string | null;
output: boolean;
escaped: boolean;
aliased: boolean;
}
export interface IError {
name: string;
explore: IExplore;
messages: string[];
}
export const analyze = (props: IProps): ValidationPipe<Metadata, IError> => {
const errors: IError[] = [];
const metadata: Metadata = explore_metadata({
...props,
errors,
explore: {
top: true,
object: null,
property: null,
parameter: null,
nested: null,
aliased: false,
escaped: false,
output: false,
},
intersected: false,
});
iterate_metadata_collection({
errors,
collection: props.collection,
});
iterate_metadata_sort({
collection: props.collection,
metadata: metadata,
});
if (props.options.validate)
errors.push(
...validate({
transformer: props.transformer,
options: props.options,
functor: props.options.validate,
metadata,
}),
);
return errors.length
? {
success: false,
errors,
}
: {
success: true,
data: metadata,
};
};
/**
* @internal
*/
export const soleLiteral = (value: string): Metadata => {
const meta: Metadata = Metadata.initialize();
meta.constants.push(
MetadataConstant.from({
values: [
{
value,
tags: [],
},
],
type: "string",
}),
);
return meta;
};
export const validate = (props: {
transformer?: ts.TransformationContext;
options: IOptions;
functor: Validator;
metadata: Metadata;
}): IError[] => {
const visitor: IValidationVisitor = {
functor: props.functor,
errors: [],
objects: new Set(),
arrays: new Set(),
tuples: new Set(),
aliases: new Set(),
functions: new Set(),
};
validateMeta({
...props,
visitor,
explore: {
object: null,
property: null,
parameter: null,
nested: null,
top: true,
aliased: false,
escaped: false,
output: false,
},
});
return visitor.errors;
};
const validateMeta = (props: {
options: IOptions;
visitor: IValidationVisitor;
metadata: Metadata;
explore: IExplore;
}) => {
const result: string[] = [];
for (const atomic of props.metadata.atomics)
for (const row of atomic.tags)
for (const tag of row.filter(
(t) => t.validate !== undefined && t.predicate === undefined,
))
try {
tag.predicate = ExpressionFactory.transpile({
script: tag.validate!,
});
} catch {
result.push(
`Unable to transpile type tag script: ${JSON.stringify(
tag.validate,
)}`,
);
tag.predicate = () => ts.factory.createTrue();
}
result.push(...props.visitor.functor(props.metadata, props.explore));
if (result.length)
props.visitor.errors.push({
name: props.metadata.getName(),
explore: { ...props.explore },
messages: [...new Set(result)],
});
for (const alias of props.metadata.aliases)
validateAlias({
...props,
alias: alias.type,
});
for (const array of props.metadata.arrays)
validateArray({
...props,
array: array.type,
});
for (const tuple of props.metadata.tuples)
validateTuple({
...props,
tuple: tuple.type,
});
for (const object of props.metadata.objects)
validateObject({
...props,
object: object.type,
});
for (const func of props.metadata.functions)
validateFunction({
...props,
function: func,
});
for (const set of props.metadata.sets)
validateMeta({
...props,
metadata: set.value,
});
for (const map of props.metadata.maps) {
validateMeta({
...props,
metadata: map.key,
});
validateMeta({
...props,
metadata: map.value,
});
}
if (props.options.escape === true && props.metadata.escaped !== null)
validateMeta({
...props,
metadata: props.metadata.escaped.returns,
explore: {
...props.explore,
escaped: true,
},
});
};
const validateAlias = (props: {
transformer?: ts.TransformationContext;
options: IOptions;
visitor: IValidationVisitor;
alias: MetadataAliasType;
explore: IExplore;
}) => {
if (props.visitor.aliases.has(props.alias)) return;
props.visitor.aliases.add(props.alias);
validateMeta({
...props,
metadata: props.alias.value,
explore: {
...props.explore,
nested: props.alias,
aliased: true,
},
});
};
const validateArray = (props: {
transformer?: ts.TransformationContext;
options: IOptions;
visitor: IValidationVisitor;
array: MetadataArrayType;
explore: IExplore;
}) => {
if (props.visitor.arrays.has(props.array)) return;
props.visitor.arrays.add(props.array);
validateMeta({
...props,
metadata: props.array.value,
explore: {
...props.explore,
nested: props.array,
top: false,
},
});
};
const validateTuple = (props: {
transformer?: ts.TransformationContext;
options: IOptions;
visitor: IValidationVisitor;
tuple: MetadataTupleType;
explore: IExplore;
}) => {
if (props.visitor.tuples.has(props.tuple)) return;
props.visitor.tuples.add(props.tuple);
for (const elem of props.tuple.elements)
validateMeta({
...props,
metadata: elem,
explore: {
...props.explore,
nested: props.tuple,
top: false,
},
});
};
const validateObject = (props: {
transformer?: ts.TransformationContext;
options: IOptions;
visitor: IValidationVisitor;
object: MetadataObjectType;
}) => {
if (props.visitor.objects.has(props.object)) return;
props.visitor.objects.add(props.object);
if (props.options.validate) {
const explore: IExplore = {
object: props.object,
top: false,
property: null,
parameter: null,
nested: null,
aliased: false,
escaped: false,
output: false,
};
const errors: string[] = props.options.validate(
Metadata.create({
...Metadata.initialize(),
objects: [
MetadataObject.create({
type: props.object,
tags: [],
}),
],
}),
explore,
);
if (errors.length)
props.visitor.errors.push({
name: props.object.name,
explore,
messages: [...new Set(errors)],
});
}
for (const property of props.object.properties)
validateMeta({
...props,
metadata: property.value,
explore: {
object: props.object,
property: property.key.isSoleLiteral()
? property.key.getSoleLiteral()!
: {},
parameter: null,
nested: null,
top: false,
aliased: false,
escaped: false,
output: false,
},
});
};
const validateFunction = (props: {
transformer?: ts.TransformationContext;
options: IOptions;
visitor: IValidationVisitor;
function: MetadataFunction;
explore: IExplore;
}) => {
if (props.visitor.functions.has(props.function)) return;
props.visitor.functions.add(props.function);
for (const param of props.function.parameters)
validateMeta({
...props,
metadata: param.type,
explore: {
...props.explore,
parameter: param.name,
nested: null,
top: false,
output: false,
},
});
if (props.function.output)
validateMeta({
...props,
metadata: props.function.output,
explore: {
...props.explore,
parameter: null,
nested: null,
top: false,
output: true,
},
});
};
interface IValidationVisitor {
functor: Validator;
errors: IError[];
objects: Set<MetadataObjectType>;
arrays: Set<MetadataArrayType>;
tuples: Set<MetadataTupleType>;
aliases: Set<MetadataAliasType>;
functions: Set<MetadataFunction>;
}
}