typia
Version:
Superfast runtime validators with only one line
876 lines (833 loc) • 27.8 kB
text/typescript
import ts from "typescript";
import { IMetadataTypeTag } from "../schemas/metadata/IMetadataTypeTag";
import { Metadata } from "../schemas/metadata/Metadata";
import { MetadataObjectType } from "../schemas/metadata/MetadataObjectType";
import { MetadataProperty } from "../schemas/metadata/MetadataProperty";
import { IProtobufProperty } from "../schemas/protobuf/IProtobufProperty";
import { IProtobufPropertyType } from "../schemas/protobuf/IProtobufPropertyType";
import { IProtobufSchema } from "../schemas/protobuf/IProtobufSchema";
import { ProtobufUtil } from "../programmers/helpers/ProtobufUtil";
import { TransformerError } from "../transformers/TransformerError";
import { ProtobufAtomic } from "../typings/ProtobufAtomic";
import { ValidationPipe } from "../typings/ValidationPipe";
import { MetadataCollection } from "./MetadataCollection";
import { MetadataFactory } from "./MetadataFactory";
export namespace ProtobufFactory {
export interface IProps {
method: string;
checker: ts.TypeChecker;
transformer?: ts.TransformationContext;
collection: MetadataCollection;
type: ts.Type;
}
/* -----------------------------------------------------------
METADATA COMPOSER
----------------------------------------------------------- */
export const metadata = (props: IProps): Metadata => {
// COMPOSE METADATA WITH INDIVIDUAL VALIDATIONS
const result: ValidationPipe<Metadata, MetadataFactory.IError> =
MetadataFactory.analyze({
...props,
transformer: props.transformer,
options: {
escape: false,
constant: true,
absorb: true,
validate: validate(),
},
});
if (result.success === false)
throw TransformerError.from({
code: `typia.protobuf.${props.method}`,
errors: result.errors,
});
return result.data;
};
/**
* @internal
*/
export const emplaceObject = (object: MetadataObjectType): void => {
for (const p of object.properties) emplaceProperty(p);
const properties: IProtobufProperty[] = object.properties
.map((p) => p.of_protobuf_)
.filter((p) => p !== undefined);
const unique: Set<number> = new Set(
properties
.filter((p) => p !== undefined)
.filter((p) => p.fixed === true)
.map((p) => p.union.map((u) => u.index))
.flat(),
);
let index: number = 1;
properties.forEach((schema) => {
if (schema.fixed === true)
index = Math.max(
index,
Math.max(...schema.union.map((u) => u.index)) + 1,
);
else {
for (const u of schema.union) {
while (unique.has(index) === true) ++index;
u.index = index;
unique.add(index);
}
++index;
}
});
};
const emplaceProperty = (prop: MetadataProperty): void => {
const union: IProtobufPropertyType[] = [];
for (const native of prop.value.natives)
if (native.name === "Uint8Array")
union.push({
type: "bytes",
index: ProtobufUtil.getSequence(native.tags[0] ?? [])!,
});
union.push(...emplaceAtomic(prop.value).values());
for (const array of prop.value.arrays)
union.push({
type: "array",
array: array.type,
value: emplaceSchema(
array.type.value,
) as IProtobufSchema.IArray["value"],
index: ProtobufUtil.getSequence(array.tags[0] ?? [])!,
});
for (const obj of prop.value.objects)
if (isDynamicObject(obj.type))
union.push({
type: "map",
map: obj.type,
key: emplaceSchema(
obj.type.properties[0]!.key,
) as IProtobufSchema.IMap["key"],
value: emplaceSchema(
obj.type.properties[0]!.value,
) as IProtobufSchema.IMap["value"],
index: ProtobufUtil.getSequence(obj.tags[0] ?? [])!,
});
else
union.push({
type: "object",
object: obj.type,
index: ProtobufUtil.getSequence(obj.tags[0] ?? [])!,
});
for (const map of prop.value.maps)
union.push({
type: "map",
map,
key: emplaceSchema(map.key) as IProtobufSchema.IMap["key"],
value: emplaceSchema(map.value) as IProtobufSchema.IMap["value"],
index: ProtobufUtil.getSequence(map.tags[0] ?? [])!,
});
prop.of_protobuf_ = {
union,
fixed: union.every((p) => p.index !== null),
};
};
const emplaceSchema = (metadata: Metadata): IProtobufSchema => {
for (const native of metadata.natives)
if (native.name === "Uint8Array")
return {
type: "bytes",
};
const atomic = emplaceAtomic(metadata);
if (atomic.size) return atomic.values().next().value!;
for (const array of metadata.arrays)
return {
type: "array",
array: array.type,
value: emplaceSchema(
array.type.value,
) as IProtobufSchema.IArray["value"],
};
for (const obj of metadata.objects)
if (isDynamicObject(obj.type))
return {
type: "map",
map: obj.type,
key: emplaceSchema(
obj.type.properties[0]!.key,
) as IProtobufSchema.IMap["key"],
value: emplaceSchema(
obj.type.properties[0]!.value,
) as IProtobufSchema.IMap["value"],
};
else
return {
type: "object",
object: obj.type,
};
for (const map of metadata.maps)
return {
type: "map",
map,
key: emplaceSchema(map.key) as IProtobufSchema.IMap["key"],
value: emplaceSchema(map.value) as IProtobufSchema.IMap["value"],
};
throw new Error(
"Error on ProtobufFactory.emplaceSchema(): any type detected.",
);
};
const emplaceAtomic = (
meta: Metadata,
): Map<ProtobufAtomic, IProtobufPropertyType> => {
const map: Map<ProtobufAtomic, IProtobufPropertyType> = new Map();
// CONSTANTS
for (const c of meta.constants)
if (c.type === "boolean")
map.set("bool", {
type: "bool",
index: getSequence(c.values[0]?.tags[0] ?? [])!,
});
else if (c.type === "bigint") {
const init: ProtobufAtomic.BigNumeric = getBigintType(
c.values.map((v) => BigInt(v.value)),
);
for (const value of c.values)
emplaceBigint({
map,
tags: value.tags,
init,
});
} else if (c.type === "number") {
const init: ProtobufAtomic.Numeric = getNumberType(
c.values.map((v) => v.value) as number[],
);
for (const value of c.values)
emplaceNumber({
map,
tags: value.tags,
init,
});
} else if (c.type === "string")
map.set("string", {
type: "string",
index: getSequence(c.values[0]?.tags[0] ?? [])!,
});
// TEMPLATE
if (meta.templates.length)
map.set("string", {
type: "string",
index: getSequence(meta.templates[0]?.tags[0] ?? [])!,
});
// ATOMICS
for (const atomic of meta.atomics)
if (atomic.type === "boolean")
map.set("bool", {
type: "bool",
index: getSequence(atomic.tags[0] ?? [])!,
});
else if (atomic.type === "bigint")
emplaceBigint({
map,
tags: atomic.tags,
init: "int64",
});
else if (atomic.type === "number")
emplaceNumber({
map,
tags: atomic.tags,
init: "double",
});
else if (atomic.type === "string")
map.set("string", {
type: "string",
index: getSequence(atomic.tags[0] ?? [])!,
});
// SORTING FOR VALIDATION REASON
return new Map(
Array.from(map).sort((x, y) => ProtobufUtil.compare(x[0], y[0])),
);
};
const emplaceBigint = (next: {
map: Map<ProtobufAtomic, IProtobufPropertyType>;
tags: IMetadataTypeTag[][];
init: ProtobufAtomic.BigNumeric;
}): void => {
if (next.tags.length === 0) {
next.map.set(next.init, {
type: "bigint",
name: next.init,
index: null!,
});
return;
}
for (const row of next.tags) {
const value: ProtobufAtomic.BigNumeric =
row.find(
(tag) =>
tag.kind === "type" &&
(tag.value === "int64" || tag.value === "uint64"),
)?.value ?? next.init;
next.map.set(next.init, {
type: "bigint",
name: value,
index: ProtobufUtil.getSequence(row)!,
});
}
};
const emplaceNumber = (next: {
map: Map<ProtobufAtomic, IProtobufPropertyType>;
tags: IMetadataTypeTag[][];
init: ProtobufAtomic.Numeric;
}): void => {
if (next.tags.length === 0) {
next.map.set(next.init, {
type: "number",
name: next.init,
index: null!,
});
return;
}
for (const row of next.tags) {
const value: ProtobufAtomic.Numeric =
row.find(
(tag) =>
tag.kind === "type" &&
(tag.value === "int32" ||
tag.value === "uint32" ||
tag.value === "int64" ||
tag.value === "uint64" ||
tag.value === "float" ||
tag.value === "double"),
)?.value ?? next.init;
next.map.set(value, {
type: "number",
name: value,
index: ProtobufUtil.getSequence(row)!,
});
}
};
const getBigintType = (values: bigint[]): ProtobufAtomic.BigNumeric =>
values.some((v) => v < 0) ? "int64" : "uint64";
const getNumberType = (values: number[]): ProtobufAtomic.Numeric =>
values.every((v) => Math.floor(v) === v)
? values.every((v) => -2147483648 <= v && v <= 2147483647)
? "int32"
: "int64"
: "double";
const getSequence = (tags: IMetadataTypeTag[]): number | null => {
const sequence = tags.find(
(t) =>
t.kind === "sequence" &&
typeof (t.schema as any)?.["x-protobuf-sequence"] === "number",
);
if (sequence === undefined) return null;
const value: number = Number(
(sequence.schema as any)["x-protobuf-sequence"],
);
return Number.isNaN(value) ? null : value;
};
/* -----------------------------------------------------------
VALIDATORS
----------------------------------------------------------- */
const validate = () => {
const visited: WeakSet<MetadataObjectType> = new WeakSet();
return (meta: Metadata, explore: MetadataFactory.IExplore): string[] => {
const errors: string[] = [];
const insert = (msg: string) => errors.push(msg);
if (explore.top === true) {
const onlyObject: boolean =
meta.size() === 1 &&
meta.objects.length === 1 &&
meta.objects[0]!.type.properties.every((p) =>
p.key.isSoleLiteral(),
) &&
meta.isRequired() === true &&
meta.nullable === false;
if (onlyObject === false)
insert("target type must be a sole and static object type");
}
for (const obj of meta.objects) {
if (visited.has(obj.type)) continue;
visited.add(obj.type);
validateObject({
object: obj.type,
errors,
});
try {
emplaceObject(obj.type);
} catch {}
}
//----
// NOT SUPPORTED TYPES
//----
const noSupport = (msg: string) => insert(`does not support ${msg}`);
// PROHIBIT ANY TYPE
if (meta.any) noSupport("any type");
// PROHIBIT FUNCTIONAL TYPE
if (meta.functions.length) noSupport("functional type");
// PROHIBIT TUPLE TYPE
if (meta.tuples.length) noSupport("tuple type");
// PROHIBIT SET TYPE
if (meta.sets.length) noSupport("Set type");
// NATIVE TYPE, BUT NOT Uint8Array
if (meta.natives.length)
for (const native of meta.natives) {
if (native.name === "Uint8Array") continue;
const instead = BANNED_NATIVE_TYPES.get(native.name);
if (instead === undefined) noSupport(`${native.name} type`);
else noSupport(`${native.name} type. Use ${instead} type instead.`);
}
//----
// ATOMIC CASES
//----
if (meta.atomics.length) {
const numbers = ProtobufUtil.getNumbers(meta);
const bigints = ProtobufUtil.getBigints(meta);
for (const type of ["int64", "uint64"])
if (numbers.has(type) && bigints.has(type))
insert(
`tags.Type<"${type}"> cannot be used in both number and bigint types. Recommend to remove from number type`,
);
}
//----
// ARRAY CASES
//----
// DO NOT ALLOW MULTI-DIMENSIONAL ARRAY
if (
meta.arrays.length &&
meta.arrays.some((array) => !!array.type.value.arrays.length)
)
noSupport("over two dimensional array type");
// CHILD OF ARRAY TYPE MUST BE REQUIRED
if (
meta.arrays.length &&
meta.arrays.some(
(array) =>
array.type.value.isRequired() === false ||
array.type.value.nullable === true,
)
)
noSupport("optional type in array");
// UNION IN ARRAY
if (
meta.arrays.length &&
meta.arrays.some(
(a) =>
a.type.value.size() > 1 &&
a.type.value.constants.length !== 1 &&
a.type.value.constants[0]?.values.length !== a.type.value.size(),
)
)
noSupport("union type in array");
// DO DYNAMIC OBJECT IN ARRAY
if (
meta.arrays.length &&
meta.arrays.some(
(a) =>
a.type.value.maps.length ||
(a.type.value.objects.length &&
a.type.value.objects.some(
(o) => ProtobufUtil.isStaticObject(o.type) === false,
)),
)
)
noSupport("dynamic object in array");
// UNION WITH ARRAY
if (meta.size() > 1 && meta.arrays.length)
noSupport("union type with array type");
//----
// OBJECT CASES
//----
// EMPTY PROPERTY
if (
meta.objects.length &&
meta.objects.some((obj) => obj.type.properties.length === 0)
)
noSupport("empty object type");
// MULTIPLE DYNAMIC KEY TYPED PROPERTIES
if (
meta.objects.length &&
meta.objects.some(
(obj) =>
obj.type.properties.filter((p) => !p.key.isSoleLiteral()).length >
1,
)
)
noSupport(
"object type with multiple dynamic key typed properties. Keep only one.",
);
// STATIC AND DYNAMIC PROPERTIES ARE COMPATIBLE
if (
meta.objects.length &&
meta.objects.some(
(obj) =>
obj.type.properties.some((p) => p.key.isSoleLiteral()) &&
obj.type.properties.some((p) => !p.key.isSoleLiteral()),
)
)
noSupport(
"object type with mixed static and dynamic key typed properties. Keep statics or dynamic only.",
);
// DYNAMIC OBJECT, BUT PROPERTY VALUE TYPE IS ARRAY
if (
meta.objects.length &&
isDynamicObject(meta.objects[0]!.type) &&
meta.objects[0]!.type.properties.some((p) => !!p.value.arrays.length)
)
noSupport("dynamic object with array value type");
// UNION WITH DYNAMIC OBJECTTa
if (
meta.size() > 1 &&
meta.objects.length &&
isDynamicObject(meta.objects[0]!.type)
)
noSupport("union type with dynamic object type");
// UNION IN DYNAMIC PROPERTY VALUE
if (
meta.objects.length &&
meta.objects.some(
(obj) =>
isDynamicObject(obj.type) &&
obj.type.properties.some((p) => ProtobufUtil.isUnion(p.value)),
)
)
noSupport("union type in dynamic property");
//----
// MAP CASES
//----
// KEY TYPE IS UNION
if (
meta.maps.length &&
meta.maps.some((m) => ProtobufUtil.isUnion(m.key))
)
noSupport("union key typed map");
// KEY TYPE IS NOT ATOMIC
if (
meta.maps.length &&
meta.maps.some((m) => ProtobufUtil.getAtomics(m.key).size !== 1)
)
noSupport("non-atomic key typed map");
// MAP TYPE, BUT PROPERTY KEY TYPE IS OPTIONAL
if (
meta.maps.length &&
meta.maps.some((m) => m.key.isRequired() === false || m.key.nullable)
)
noSupport("optional key typed map");
// MAP TYPE, BUT VALUE TYPE IS ARRAY
if (meta.maps.length && meta.maps.some((m) => !!m.value.arrays.length))
noSupport("map type with array value type");
// UNION WITH MAP
if (meta.size() > 1 && meta.maps.length)
noSupport("union type with map type");
// UNION IN MAP
if (
meta.maps.length &&
meta.maps.some((m) => ProtobufUtil.isUnion(m.value))
)
noSupport("union type in map value type");
return errors;
};
};
/* -----------------------------------------------------------
SEQUENE VALIDATOR
----------------------------------------------------------- */
const validateObject = (next: {
object: MetadataObjectType;
errors: string[];
}): void => {
for (const property of next.object.properties)
validateProperty({
metadata: property.value,
errors: next.errors,
});
const entire: Map<number, string> = new Map();
const visitProperty = (p: MetadataProperty) => {
const local: Set<number> = new Set();
const tagger = (matrix: IMetadataTypeTag[][]): void => {
matrix.forEach((tags) => {
const value: number | null = ProtobufUtil.getSequence(tags);
if (value !== null) local.add(value);
});
};
for (const c of p.value.constants)
for (const v of c.values) tagger(v.tags);
for (const a of p.value.atomics) tagger(a.tags);
for (const t of p.value.templates) tagger(t.tags);
for (const o of p.value.objects) tagger(o.tags);
for (const a of p.value.arrays) tagger(a.tags);
for (const s of local)
if (entire.has(s))
next.errors.push(
`The Sequence<${s}> tag is duplicated in two properties (${JSON.stringify(entire.get(s))} and ${JSON.stringify(p.key.getSoleLiteral())})`,
);
else entire.set(s, p.key.getSoleLiteral()!);
};
for (const p of next.object.properties) visitProperty(p);
};
const validateProperty = (next: {
metadata: Metadata;
errors: string[];
}): void => {
let expected: number = 0;
const sequences: Set<number> = new Set();
const add = (value: number): boolean => {
if (sequences.has(value)) return false;
sequences.add(value);
++expected;
return true;
};
for (const validator of [
validateBooleanSequence,
validateNumericSequences({
type: "bigint",
default: "int64",
categories: BIGINT_TYPES,
}),
validateNumericSequences({
type: "number",
default: "double",
categories: NUMBER_TYPES,
}),
validateStringSequence,
])
validator({ metadata: next.metadata, errors: next.errors, add });
for (const array of next.metadata.arrays)
validateInstanceSequence({
type: "array",
tags: array.tags,
errors: next.errors,
add,
});
for (const object of next.metadata.objects)
validateInstanceSequence({
type: "object",
tags: object.tags,
errors: next.errors,
add,
});
for (const map of next.metadata.maps)
validateInstanceSequence({
type: "map",
tags: map.tags,
errors: next.errors,
add,
});
for (const native of next.metadata.natives)
if (native.name === "Uint8Array")
validateInstanceSequence({
type: "Uint8Array",
tags: native.tags,
errors: next.errors,
add,
});
};
const validateBooleanSequence = (next: {
metadata: Metadata;
errors: string[];
add: (value: number) => boolean;
}): void => {
// PREPARE EMPLACER
const unique: Set<number> = new Set();
let expected: number = 0;
let actual: number = 0;
const emplace = (matrix: IMetadataTypeTag[][]): void => {
for (const tags of matrix)
for (const tag of tags) {
const sequence = ProtobufUtil.getSequence([tag]);
if (sequence !== null) {
unique.add(sequence);
++actual;
}
++expected;
}
};
// GATHER SEQUENCE TAGS
for (const atomic of next.metadata.atomics)
if (atomic.type === "boolean") emplace(atomic.tags);
for (const constant of next.metadata.constants)
if (constant.type === "boolean")
for (const value of constant.values) emplace(value.tags);
// PREDICATE
if (unique.size && actual !== expected)
next.errors.push(
`The sequence tag must be declared in every union type members`,
);
else if (unique.size > 1)
next.errors.push(
`The sequence tag value must be the same in boolean type (including literal types)`,
);
else if (unique.size === 1) {
const value: number = unique.values().next().value!;
if (next.add(value) === false)
next.errors.push(
`The sequence tag value ${value} in boolean type is duplicated with other types`,
);
}
};
const validateNumericSequences =
(config: {
type: "number" | "bigint";
default: string;
categories: Set<string>;
}) =>
(next: {
metadata: Metadata;
errors: string[];
add: (value: number) => boolean;
}): void => {
// FIND TYPE CATEGORIES
const categories: Set<string> = new Set();
const getType = (tags: IMetadataTypeTag[]): string => {
const found: IMetadataTypeTag | undefined = tags.find(
(t) => t.kind === "type" && config.categories.has(t.value),
);
return found?.value ?? config.default;
};
const exploreCategory = (matrix: IMetadataTypeTag[][]): void => {
for (const tags of matrix) categories.add(getType(tags));
};
for (const atomic of next.metadata.atomics)
if (atomic.type === config.type) exploreCategory(atomic.tags);
for (const constant of next.metadata.constants)
if (constant.type === config.type)
for (const value of constant.values) exploreCategory(value.tags);
// ITERATE TYPE CATEGORIES
for (const category of categories) {
const unique: Set<number> = new Set();
let expected: number = 0;
let actual: number = 0;
const emplace = (tags: IMetadataTypeTag[]): void => {
const sequence: number | null = ProtobufUtil.getSequence(tags);
if (sequence !== null) {
unique.add(sequence);
++actual;
}
++expected;
};
for (const atomic of next.metadata.atomics)
if (atomic.type === config.type)
for (const tags of atomic.tags)
if (getType(tags) === category) emplace(tags);
for (const constant of next.metadata.constants)
if (constant.type === config.type)
for (const value of constant.values)
for (const tags of value.tags)
if (getType(tags) === category) emplace(tags);
if (unique.size && actual !== expected) {
next.errors.push(
`The sequence tag must be declared in every union type members`,
);
} else if (unique.size > 1)
next.errors.push(
`The sequence tag value must be the same in ${config.type} type (including literal types)`,
);
else if (unique.size === 1) {
const value: number = unique.values().next().value!;
if (next.add(value) === false)
next.errors.push(
`The sequence tag value ${value} in ${config.type} type is duplicated with other types`,
);
}
}
};
const validateStringSequence = (next: {
metadata: Metadata;
errors: string[];
add: (value: number) => boolean;
}): void => {
const unique: Set<number> = new Set();
let expected: number = 0;
let actual: number = 0;
const emplace = (matrix: IMetadataTypeTag[][]): void => {
for (const tags of matrix)
for (const tag of tags) {
const sequence = ProtobufUtil.getSequence([tag]);
if (sequence !== null) {
unique.add(sequence);
++actual;
}
++expected;
}
};
for (const atomic of next.metadata.atomics)
if (atomic.type === "string") emplace(atomic.tags);
for (const constant of next.metadata.constants)
if (constant.type === "string")
for (const value of constant.values) emplace(value.tags);
for (const template of next.metadata.templates) emplace(template.tags);
if (unique.size && actual !== expected)
next.errors.push(
`The sequence tag must be declared in every union type members`,
);
else if (unique.size > 1)
next.errors.push(
`The sequence tag value must be the same in string types including literal and template types`,
);
else if (unique.size === 1) {
const value: number = unique.values().next().value!;
if (next.add(value) === false)
next.errors.push(
`The sequence tag value ${value} in string type is duplicated with other types`,
);
}
};
const validateInstanceSequence = (next: {
type: "array" | "object" | "map" | "Uint8Array";
tags: IMetadataTypeTag[][];
errors: string[];
add: (value: number) => boolean;
}): void => {
const unique: Set<number> = new Set();
let count: number = 0;
for (const tags of next.tags) {
const value: number | null = ProtobufUtil.getSequence(tags);
if (value === null) continue;
unique.add(value);
++count;
}
if (unique.size && count !== next.tags.length)
next.errors.push(
`The sequence tag must be declared in every union type members`,
);
else if (unique.size > 1)
next.errors.push(
`The sequence tag value must be the same in ${next.type === "array" ? "an array" : "object"} type.`,
);
else if (unique.size === 1) {
const value: number = unique.values().next().value!;
if (next.add(value) === false)
next.errors.push(
`The sequence tag value ${value} in ${next.type} type is duplicated with other types`,
);
}
};
}
const isDynamicObject = (obj: MetadataObjectType): boolean =>
obj.properties[0]!.key.isSoleLiteral() === false;
const BANNED_NATIVE_TYPES: Map<string, string | null> = new Map([
["Date", "string"],
["Boolean", "boolean"],
["BigInt", "bigint"],
["Number", "number"],
["String", "string"],
...[
"Buffer",
"Uint8ClampedArray",
"Uint16Array",
"Uint32Array",
"BigUint64Array",
"Int8Array",
"Int16Array",
"Int32Array",
"BigInt64Array",
"Float32Array",
"Float64Array",
"DataView",
"ArrayBuffer",
"SharedArrayBuffer",
].map((name) => [name, "Uint8Array"] as const),
["WeakSet", "Array"],
["WeakMap", "Map"],
]);
const NUMBER_TYPES: Set<string> = new Set([
"int32",
"uint32",
"int64",
"uint64",
"float",
"double",
]);
const BIGINT_TYPES = new Set(["int64", "uint64"]);