typia
Version:
Superfast runtime validators with only one line
214 lines (202 loc) • 7.15 kB
text/typescript
import ts from "typescript";
import { IMetadataTypeTag } from "../../../schemas/metadata/IMetadataTypeTag";
import { Metadata } from "../../../schemas/metadata/Metadata";
import { MetadataAtomic } from "../../../schemas/metadata/MetadataAtomic";
import { MetadataConstant } from "../../../schemas/metadata/MetadataConstant";
import { MetadataConstantValue } from "../../../schemas/metadata/MetadataConstantValue";
import { MetadataTemplate } from "../../../schemas/metadata/MetadataTemplate";
import { ArrayUtil } from "../../../utils/ArrayUtil";
import { MetadataCollection } from "../../MetadataCollection";
import { MetadataFactory } from "../../MetadataFactory";
import { MetadataTypeTagFactory } from "../../MetadataTypeTagFactory";
import { explore_metadata } from "./explore_metadata";
import { iterate_metadata } from "./iterate_metadata";
import { iterate_metadata_array } from "./iterate_metadata_array";
export const iterate_metadata_intersection =
(checker: ts.TypeChecker) =>
(options: MetadataFactory.IOptions) =>
(collection: MetadataCollection) =>
(errors: MetadataFactory.IError[]) =>
(
meta: Metadata,
type: ts.Type,
explore: MetadataFactory.IExplore,
): boolean => {
if (!type.isIntersection()) return false;
if (
// ONLY OBJECT TYPED INTERSECTION
type.types.every(
(child) =>
(child.getFlags() & ts.TypeFlags.Object) !== 0 &&
!checker.isArrayType(child) &&
!checker.isTupleType(child),
)
)
return false;
// COSTRUCT FAKE METADATA LIST
const fakeCollection: MetadataCollection = collection.clone();
const fakeErrors: MetadataFactory.IError[] = [];
const children: Metadata[] = [
...new Map(
type.types.map((t) => {
const m: Metadata = explore_metadata(checker)({
...options,
absorb: true,
})(fakeCollection)(fakeErrors)(t, {
...explore,
aliased: false,
});
return [m.getName(), m] as const;
}),
).values(),
];
if (fakeErrors.length) {
errors.push(...fakeErrors);
return true;
}
// ONLY ONE CHILD AFTER REMOVING DUPLICATES
if (children.length === 1) {
iterate_metadata(checker)(options)(collection)(errors)(
meta,
type.types[0]!,
explore,
);
return true;
} else if (children.every((c) => c.objects.length === c.size()))
// ONLY OBJECT TYPED INTERSECTION (DETAILED)
return false;
// VALIDATE EACH TYPES
const nonsensible = () => {
errors.push({
name: children.map((c) => c.getName()).join(" & "),
explore: { ...explore },
messages: ["nonsensible intersection"],
});
return true;
};
const individuals: (readonly [Metadata, number])[] = children
.map((child, i) => [child, i] as const)
.filter(
([c]) =>
(c.size() === 1 &&
(c.atomics.length === 1 ||
(c.constants.length === 1 &&
c.constants[0]!.values.length === 1) ||
c.arrays.length === 1)) ||
c.templates.length === 1,
);
if (individuals.length !== 1) return nonsensible();
const objects: Metadata[] = children.filter(
(c) =>
c.nullable === false &&
c.isRequired() === true &&
c.objects.length &&
c.objects.length === c.size() &&
c.objects.every((o) => o.properties.every((p) => p.value.optional)),
);
const arrays: Set<string> = new Set(
individuals.map(([c]) => c.arrays.map((a) => a.type.name)).flat(),
);
const atomics: Set<"boolean" | "bigint" | "number" | "string"> = new Set(
individuals.map(([c]) => [...c.atomics.map((a) => a.type)]).flat(),
);
const constants: Metadata[] = individuals
.filter((i) => i[0].constants.length === 1)
.map(([m]) => m);
const templates: Metadata[] = individuals
.filter((i) => i[0].templates.length === 1)
.map(([m]) => m);
// ESCAPE WHEN ONLY CONSTANT TYPES EXIST
if (
atomics.size + constants.length + arrays.size + templates.length > 1 ||
individuals.length + objects.length !== children.length
)
return nonsensible();
// RE-GENERATE TYPE
const target: "boolean" | "bigint" | "number" | "string" | "array" =
arrays.size
? "array"
: atomics.size
? atomics.values().next().value!
: constants.length
? constants[0]!.constants[0]!.type
: "string";
if (target === "array") {
const name: string = arrays.values().next().value!;
if (!meta.arrays.some((a) => a.type.name === name)) {
iterate_metadata_array(checker)(options)(collection)(errors)(
meta,
type.types[individuals.find((i) => i[0].arrays.length === 1)![1]]!,
{
...explore,
aliased: false,
escaped: false,
},
);
}
} else if (atomics.size)
ArrayUtil.add(
meta.atomics,
MetadataAtomic.create({
type: atomics.values().next().value as "string",
tags: [],
}),
(a, b) => a.type === b.type,
);
else if (constants.length)
ArrayUtil.take(
ArrayUtil.take(
meta.constants,
(o) => o.type === target,
() =>
MetadataConstant.create({
type: target,
values: [],
}),
).values,
(o) => o.value === constants[0]!.constants[0]!.values[0]!.value,
() =>
MetadataConstantValue.create({
value: constants[0]!.constants[0]!.values[0]!.value,
tags: [],
}),
);
else if (templates.length)
ArrayUtil.take(
meta.templates,
(o) => o.getBaseName() === templates[0]!.templates[0]!.getBaseName(),
() =>
MetadataTemplate.create({
row: templates[0]!.templates[0]!.row,
tags: [],
}),
);
// ASSIGN TAGS
if (objects.length) {
const tags: IMetadataTypeTag[] = MetadataTypeTagFactory.analyze(errors)(
target,
)(objects.map((om) => om.objects).flat(), explore);
if (tags.length)
if (target === "array") meta.arrays.at(-1)!.tags.push(tags);
else if (atomics.size)
meta.atomics.find((a) => a.type === target)!.tags.push(tags);
else if (constants.length) {
const constant: MetadataConstant = meta.constants.find(
(c) => c.type === target,
)!;
const value: MetadataConstantValue = constant.values.find(
(v) => v.value === constants[0]!.constants[0]!.values[0]!.value,
)!;
value.tags ??= [];
value.tags.push(tags);
} else if (templates.length) {
const template: MetadataTemplate = meta.templates.find(
(t) =>
t.getBaseName() === templates[0]!.templates[0]!.getBaseName(),
)!;
template.tags ??= [];
template.tags.push(tags);
}
}
return true;
};