typia
Version:
Superfast runtime validators with only one line
688 lines (622 loc) • 20.5 kB
text/typescript
import { ClassProperties } from "../../typings/ClassProperties";
import { ArrayUtil } from "../../utils/ArrayUtil";
import { IMetadata } from "./IMetadata";
import { IMetadataDictionary } from "./IMetadataDictionary";
import { IMetadataTypeTag } from "./IMetadataTypeTag";
import { MetadataAlias } from "./MetadataAlias";
import { MetadataArray } from "./MetadataArray";
import { MetadataAtomic } from "./MetadataAtomic";
import { MetadataConstant } from "./MetadataConstant";
import { MetadataEscaped } from "./MetadataEscaped";
import { MetadataFunction } from "./MetadataFunction";
import { MetadataMap } from "./MetadataMap";
import { MetadataNative } from "./MetadataNative";
import { MetadataObject } from "./MetadataObject";
import { MetadataObjectType } from "./MetadataObjectType";
import { MetadataSet } from "./MetadataSet";
import { MetadataTemplate } from "./MetadataTemplate";
import { MetadataTuple } from "./MetadataTuple";
export class Metadata {
public any: boolean;
public required: boolean;
public optional: boolean;
public nullable: boolean;
public escaped: MetadataEscaped | null;
public atomics: MetadataAtomic[];
public constants: MetadataConstant[];
public templates: MetadataTemplate[];
public rest: Metadata | null;
public aliases: MetadataAlias[];
public arrays: MetadataArray[];
public tuples: MetadataTuple[];
public objects: MetadataObject[];
public functions: MetadataFunction[];
public natives: MetadataNative[];
public sets: MetadataSet[];
public maps: MetadataMap[];
/** @internal */ private name_?: string;
/** @internal */ private parent_resolved_: boolean = false;
/** @internal */ public union_index?: number;
/** @internal */ public fixed_?: number | null;
/** @internal */ public boolean_literal_intersected_?: boolean;
/** @internal */ private is_sequence_?: boolean;
/* -----------------------------------------------------------
CONSTRUCTORS
----------------------------------------------------------- */
/**
* @hidden
*/
private constructor(props: ClassProperties<Metadata>) {
this.any = props.any;
this.required = props.required;
this.optional = props.optional;
this.nullable = props.nullable;
this.functions = props.functions;
this.escaped = props.escaped;
this.atomics = props.atomics;
this.constants = props.constants;
this.templates = props.templates;
this.rest = props.rest;
this.arrays = props.arrays;
this.tuples = props.tuples;
this.objects = props.objects;
this.aliases = props.aliases;
this.natives = props.natives;
this.sets = props.sets;
this.maps = props.maps;
}
/**
* @internal
*/
public static create(props: ClassProperties<Metadata>): Metadata {
return new Metadata(props);
}
/**
* @internal
*/
public static initialize(parentResolved: boolean = false): Metadata {
const meta: Metadata = Metadata.create({
any: false,
nullable: false,
required: true,
optional: false,
escaped: null,
constants: [],
atomics: [],
templates: [],
arrays: [],
tuples: [],
objects: [],
aliases: [],
functions: [],
rest: null,
natives: [],
sets: [],
maps: [],
});
meta.parent_resolved_ = parentResolved;
return meta;
}
public toJSON(): IMetadata {
return {
any: this.any,
required: this.required,
optional: this.optional,
nullable: this.nullable,
functions: this.functions.map((f) => f.toJSON()),
atomics: this.atomics.map((a) => a.toJSON()),
constants: this.constants.map((c) => c.toJSON()),
templates: this.templates.map((tpl) => tpl.toJSON()),
escaped: this.escaped ? this.escaped.toJSON() : null,
rest: this.rest ? this.rest.toJSON() : null,
arrays: this.arrays.map((array) => array.toJSON()),
tuples: this.tuples.map((tuple) => tuple.toJSON()),
objects: this.objects.map((obj) => obj.toJSON()),
aliases: this.aliases.map((alias) => alias.toJSON()),
natives: this.natives.map((native) => native.toJSON()),
sets: this.sets.map((set) => set.toJSON()),
maps: this.maps.map((map) => map.toJSON()),
};
}
public static from(meta: IMetadata, dict: IMetadataDictionary): Metadata {
return Metadata.create({
any: meta.any,
required: meta.required,
optional: meta.optional,
nullable: meta.nullable,
functions: meta.functions.map((f) => MetadataFunction.from(f, dict)),
constants: meta.constants.map(MetadataConstant.from),
atomics: meta.atomics.map(MetadataAtomic.from),
templates: meta.templates.map((tpl) => MetadataTemplate.from(tpl, dict)),
escaped: meta.escaped ? MetadataEscaped.from(meta.escaped, dict) : null,
rest: meta.rest ? this.from(meta.rest, dict) : null,
arrays: meta.arrays.map((ref) => {
const type = dict.arrays.get(ref.name);
if (type === undefined)
throw new RangeError(
`Error on Metadata.from(): failed to find array "${ref.name}".`,
);
return MetadataArray.create({
type,
tags: ref.tags.map((row) => row.slice()),
});
}),
tuples: meta.tuples.map((t) => {
const type = dict.tuples.get(t.name);
if (type === undefined)
throw new RangeError(
`Error on Metadata.from(): failed to find tuple "${t.name}".`,
);
return MetadataTuple.create({
type,
tags: t.tags.map((r) => r.slice()),
});
}),
objects: meta.objects.map((obj) => {
const found = dict.objects.get(obj.name);
if (found === undefined)
throw new RangeError(
`Error on Metadata.from(): failed to find object "${name}".`,
);
return MetadataObject.create({
type: found,
tags: obj.tags.map((r) => r.slice()),
});
}),
aliases: meta.aliases.map((alias) => {
const type = dict.aliases.get(alias.name);
if (type === undefined)
throw new RangeError(
`Error on Metadata.from(): failed to find alias "${alias}".`,
);
return MetadataAlias.create({
type,
tags: alias.tags.map((r) => r.slice()),
});
}),
natives: meta.natives.map((native) => MetadataNative.create(native)),
sets: meta.sets.map((set) =>
MetadataSet.create({
value: Metadata.from(set.value, dict),
tags: set.tags.map((r) => r.slice()),
}),
),
maps: meta.maps.map((map) =>
MetadataMap.create({
key: this.from(map.key, dict),
value: this.from(map.value, dict),
tags: map.tags.map((r) => r.slice()),
}),
),
});
}
/* -----------------------------------------------------------
ACCESSORS
----------------------------------------------------------- */
public getName(): string {
return (this.name_ ??= getName(this));
}
public empty(): boolean {
return this.bucket() === 0 || this.size() === 0;
}
public size(): number {
return (
(this.any ? 1 : 0) +
(this.escaped ? 1 : 0) +
(this.rest ? this.rest.size() : 0) +
this.templates.length +
this.atomics.length +
this.constants.map((c) => c.values.length).reduce((x, y) => x + y, 0) +
this.arrays.length +
this.tuples.length +
this.natives.length +
this.maps.length +
this.sets.length +
this.objects.length +
this.functions.length +
this.aliases.length
);
}
public bucket(): number {
return (
(this.any ? 1 : 0) +
(this.escaped ? 1 : 0) +
(this.templates.length ? 1 : 0) +
(this.atomics.length ? 1 : 0) +
(this.constants.length ? 1 : 0) +
(this.rest ? this.rest.size() : 0) +
(this.arrays.length ? 1 : 0) +
(this.tuples.length ? 1 : 0) +
(this.natives.length ? 1 : 0) +
(this.sets.length ? 1 : 0) +
(this.maps.length ? 1 : 0) +
(this.objects.length ? 1 : 0) +
(this.functions.length ? 1 : 0) +
(this.aliases.length ? 1 : 0)
);
}
/**
* @internal
*/
public isSequence(): boolean {
return (this.is_sequence_ ??= (() => {
const exists = (tags: IMetadataTypeTag[][]): boolean =>
tags.some((row) => {
const sequence = row.find(
(t) =>
t.kind === "sequence" &&
typeof (t.schema as any)?.["x-protobuf-sequence"] === "number",
);
if (sequence === undefined) return false;
const value: number = Number(
(sequence.schema as any)["x-protobuf-sequence"],
);
return !Number.isNaN(value);
});
return (
this.atomics.some((atomic) => exists(atomic.tags)) &&
this.constants.some((c) =>
c.values.some((v) => exists(v.tags ?? [])),
) &&
this.templates.some((tpl) => exists(tpl.tags)) &&
this.arrays.some((array) => exists(array.tags)) &&
this.objects.some((object) => exists(object.tags)) &&
this.natives.some(
(native) => native.name === "Uint8Array" && exists(native.tags),
)
);
})());
}
public isConstant(): boolean {
return this.bucket() === (this.constants.length ? 1 : 0);
}
public isRequired(): boolean {
return this.required === true && this.optional === false;
}
/**
* @internal
*/
public isUnionBucket(): boolean {
const size: number = this.bucket();
const emended: number =
!!this.atomics.length && !!this.constants.length ? size - 1 : size;
return emended > 1;
}
/**
* @internal
*/
public getSoleLiteral(): string | null {
if (
this.size() === 1 &&
this.constants.length === 1 &&
this.constants[0]!.type === "string" &&
this.constants[0]!.values.length === 1
)
return this.constants[0]!.values[0]!.value as string;
else return null;
}
public isSoleLiteral(): boolean {
return this.getSoleLiteral() !== null;
}
/**
* @internal
*/
public isParentResolved(): boolean {
return this.parent_resolved_;
}
}
export namespace Metadata {
export const intersects = (x: Metadata, y: Metadata): boolean => {
// CHECK ANY & OPTIONAL
if (x.any || y.any) return true;
if (x.isRequired() === false && false === y.isRequired()) return true;
if (x.nullable === true && true === y.nullable) return true;
if (!!x.functions.length && !!y.functions.length === true) return true;
//----
// INSTANCES
//----
// ARRAYS
if (x.arrays.length && y.arrays.length) return true;
if (x.tuples.length && y.tuples.length) return true;
if (x.objects.length && y.objects.length) return true;
if (x.aliases.length && y.aliases.length) return true;
// NATIVES
if (x.natives.length && y.natives.length)
if (x.natives.some((xn) => y.natives.some((yn) => xn === yn)))
return true;
// ESCAPED
if (x.escaped && y.escaped)
return (
intersects(x.escaped.original, y.escaped.original) ||
intersects(x.escaped.returns, y.escaped.returns)
);
//----
// VALUES
//----
// ATOMICS
for (const atomic of x.atomics) {
if (y.atomics.some((ya) => atomic.type === ya.type)) return true;
if (y.constants.some((yc) => atomic.type === yc.type)) return true;
}
// CONSTANTS
for (const constant of x.constants) {
const atomic: MetadataAtomic | undefined = y.atomics.find(
(elem) => elem.type === constant.type,
);
if (atomic !== undefined) return true;
const opposite: MetadataConstant | undefined = y.constants.find(
(elem) => elem.type === constant.type,
);
if (opposite === undefined) continue;
const values: Set<any> = new Set([
...constant.values.map((e) => e.value),
...opposite.values.map((e) => e.value),
]);
if (values.size !== constant.values.length + opposite.values.length)
return true;
}
// TEMPLATES
if (!!x.templates.length && y.atomics.some((ya) => ya.type === "string"))
return true;
else if (
!!y.templates.length &&
x.atomics.some((xa) => xa.type === "string")
)
return true;
return false;
};
export const covers = (
x: Metadata,
y: Metadata,
level: number = 0,
escaped: boolean = false,
): boolean => {
// CHECK ANY
if (x === y) return false;
else if (x.any) return true;
else if (y.any) return false;
if (escaped === false) {
if (x.escaped === null && y.escaped !== null) return false;
else if (
x.escaped !== null &&
y.escaped !== null &&
(!covers(x.escaped.original, y.escaped.original, level, true) ||
!covers(x.escaped.returns, y.escaped.returns, level, true))
)
return false;
}
//----
// INSTANCES
//----
if (level === 0) {
// ARRAYS
for (const ya of y.arrays)
if (
!x.arrays.some((xa) =>
covers(xa.type.value, ya.type.value, level + 1),
)
) {
return false;
}
// TUPLES
for (const yt of y.tuples)
if (
yt.type.elements.length !== 0 &&
x.tuples.some(
(xt) =>
xt.type.elements.length >= yt.type.elements.length &&
xt.type.elements
.slice(yt.type.elements.length)
.every((xv, i) => covers(xv, yt.type.elements[i]!, level + 1)),
) === false
)
return false;
}
// OBJECTS
for (const yo of y.objects)
if (
x.objects.some((xo) => MetadataObjectType.covers(xo.type, yo.type)) ===
false
)
return false;
// ALIASES
for (const yd of y.aliases)
if (x.aliases.some((xd) => xd.type.name === yd.type.name) === false)
return false;
// NATIVES
for (const yn of y.natives)
if (x.natives.some((xn) => xn === yn) === false) return false;
// SETS
for (const ys of y.sets)
if (x.sets.some((xs) => covers(xs.value, ys.value)) === false)
return false;
//----
// VALUES
//----
// ATOMICS
if (
y.atomics.some(
(ya) => x.atomics.some((xa) => xa.type === ya.type) === false,
)
)
return false;
// CONSTANTS
for (const yc of y.constants) {
if (x.atomics.some((atom) => yc.type === atom.type)) continue;
const xc: MetadataConstant | undefined = x.constants.find(
(elem) => elem.type === yc.type,
);
if (xc === undefined) return false;
else if (
(yc.values.map((e) => e.value) as number[]).some(
(yv) => xc.values.includes(yv as never) === false,
)
)
return false;
}
// FUNCTIONAL
if (!!x.functions.length === false && !!y.functions.length) return false;
// SUCCESS
return true;
};
/**
* @internal
*/
export const merge = (x: Metadata, y: Metadata): Metadata => {
const output: Metadata = Metadata.create({
any: x.any || y.any,
nullable: x.nullable || y.nullable,
required: x.required && y.required,
optional: x.optional || y.optional,
functions: x.functions.length ? x.functions : y.functions, // @todo
escaped:
x.escaped !== null && y.escaped !== null
? MetadataEscaped.create({
original: merge(x.escaped.original, y.escaped.original),
returns: merge(x.escaped.returns, y.escaped.returns),
})
: (x.escaped ?? y.escaped),
atomics: mergeTaggedTypes({
container: x.atomics,
equals: (x, y) => x.type === y.type,
getter: (x) => x.tags,
})(y.atomics),
constants: [...x.constants],
templates: x.templates.slice(),
rest:
x.rest !== null && y.rest !== null
? merge(x.rest, y.rest)
: (x.rest ?? y.rest),
arrays: mergeTaggedTypes({
container: x.arrays,
equals: (x, y) => x.type.name === y.type.name,
getter: (x) => x.tags,
})(y.arrays),
tuples: mergeTaggedTypes({
container: x.tuples,
equals: (x, y) => x.type.name === y.type.name,
getter: (x) => x.tags,
})(y.tuples),
objects: mergeTaggedTypes({
container: x.objects,
equals: (x, y) => x.type.name === y.type.name,
getter: (x) => x.tags,
})(y.objects),
aliases: mergeTaggedTypes({
container: x.aliases,
equals: (x, y) => x.type.name === y.type.name,
getter: (x) => x.tags,
})(y.aliases),
natives: mergeTaggedTypes({
container: x.natives,
equals: (x, y) => x.name === y.name,
getter: (x) => x.tags,
})(y.natives),
sets: mergeTaggedTypes({
container: x.sets,
equals: (x, y) => x.value.getName() === y.value.getName(),
getter: (x) => x.tags,
})(y.sets),
maps: mergeTaggedTypes({
container: x.maps,
equals: (x, y) =>
x.key.getName() === y.key.getName() &&
x.value.getName() === y.value.getName(),
getter: (x) => x.tags,
})(y.maps),
});
for (const constant of y.constants) {
const target: MetadataConstant = ArrayUtil.take(
output.constants,
(elem) => elem.type === constant.type,
() =>
MetadataConstant.create({
type: constant.type,
values: [],
}),
);
for (const value of constant.values)
ArrayUtil.add(target.values, value, (a, b) => a.value === b.value);
}
return output;
};
/**
* @internal
*/
export const unalias = (w: Metadata) => {
const visited: Set<Metadata> = new Set();
while (
w.size() === 1 &&
w.nullable === false &&
w.isRequired() === true &&
w.aliases.length === 1
) {
if (visited.has(w)) break;
w = w.aliases[0]!.type.value;
visited.add(w);
}
return w;
};
}
const getName = (metadata: Metadata): string => {
if (metadata.any === true) return "any";
const elements: string[] = [];
// OPTIONAL
if (metadata.nullable === true) elements.push("null");
if (metadata.isRequired() === false) elements.push("undefined");
// ATOMIC
for (const atom of metadata.atomics) {
elements.push(atom.getName());
}
for (const constant of metadata.constants)
for (const value of constant.values) elements.push(value.getName());
for (const template of metadata.templates) elements.push(template.getName());
// NATIVES
for (const native of metadata.natives) elements.push(native.getName());
for (const set of metadata.sets) elements.push(set.getName());
for (const map of metadata.maps) elements.push(map.getName());
// INSTANCES
if (metadata.rest !== null) elements.push(`...${metadata.rest.getName()}`);
for (const tuple of metadata.tuples) elements.push(tuple.type.name);
for (const array of metadata.arrays) elements.push(array.getName());
for (const object of metadata.objects) elements.push(object.getName());
for (const alias of metadata.aliases) elements.push(alias.getName());
if (metadata.escaped !== null) elements.push(metadata.escaped.getName());
// RETURNS
if (elements.length === 0) return "unknown";
else if (elements.length === 1) return elements[0]!;
elements.sort();
return `(${elements.join(" | ")})`;
};
const mergeTaggedTypes =
<T>(props: {
container: T[];
equals: (x: T, y: T) => boolean;
getter: (x: T) => IMetadataTypeTag[][];
}) =>
(opposite: T[]) => {
const output: T[] = [...props.container];
for (const elem of opposite) {
const equal = props.container.find((x) => props.equals(x, elem));
if (equal === undefined) {
output.push(elem);
continue;
}
const matrix: string[][] = props
.getter(equal)
.map((tags) => tags.map((t) => t.name))
.sort();
for (const tags of props.getter(elem)) {
const names: string[] = tags.map((t) => t.name).sort();
if (
matrix.some(
(m) =>
m.length === names.length && m.every((s, i) => s === names[i]),
)
)
continue;
props.getter(equal).push(tags);
}
}
return output;
};