typia
Version:
Superfast runtime validators with only one line
267 lines (254 loc) • 8.99 kB
text/typescript
import { MetadataFactory } from "../../factories/MetadataFactory";
import { __IJsonApplication } from "../../schemas/json/__IJsonApplication";
import { IJsDocTagInfo } from "../../schemas/metadata/IJsDocTagInfo";
import { Metadata } from "../../schemas/metadata/Metadata";
import { MetadataFunction } from "../../schemas/metadata/MetadataFunction";
import { MetadataObjectType } from "../../schemas/metadata/MetadataObjectType";
import { MetadataProperty } from "../../schemas/metadata/MetadataProperty";
import { JsonSchemasProgrammer } from "./JsonSchemasProgrammer";
export namespace JsonApplicationProgrammer {
export const validate = (
metadata: Metadata,
explore: MetadataFactory.IExplore,
): string[] => {
if (explore.top === false) return JsonSchemasProgrammer.validate(metadata);
const output: string[] = [];
const valid: boolean =
metadata.size() === 1 &&
metadata.objects.length === 1 &&
metadata.isRequired() === true &&
metadata.nullable === false;
if (valid === false)
output.push(
"JSON application's generic argument must be a class/interface type.",
);
const object: MetadataObjectType | undefined = metadata.objects[0]?.type;
if (object !== undefined) {
if (object.properties.some((p) => p.key.isSoleLiteral() === false))
output.push("JSON application does not allow dynamic keys.");
let least: boolean = false;
for (const p of object.properties) {
const value: Metadata = p.value;
if (value.functions.length) {
least ||= true;
if (valid === false) {
if (value.functions.length !== 1 || value.size() !== 1)
output.push(
"JSON application's function type does not allow union type.",
);
if (value.isRequired() === false)
output.push("JSON application's function type must be required.");
if (value.nullable === true)
output.push(
"JSON application's function type must not be nullable.",
);
}
}
}
if (least === false)
output.push(
"JSON application's target type must have at least a function type.",
);
}
return output;
};
export const write = <Version extends "3.0" | "3.1">(props: {
version: Version;
metadata: Metadata;
filter?: (prop: MetadataProperty) => boolean;
}): __IJsonApplication<Version> => {
const object: MetadataObjectType = props.metadata.objects[0]!.type;
const definitions: Metadata[] = [];
const setters: Array<(schema: __IJsonApplication.Schema<Version>) => void> =
[];
const collect = (
metadata: Metadata,
setter: (schema: __IJsonApplication.Schema<Version>) => void,
): void => {
definitions.push(metadata);
setters.push(setter);
};
const functions: __IJsonApplication.IFunction<
__IJsonApplication.Schema<Version>
>[] = object.properties
.filter(
(p) =>
p.key.isSoleLiteral() &&
p.value.size() === 1 &&
p.value.nullable === false &&
p.value.isRequired() === true &&
Metadata.unalias(p.value).functions.length === 1,
)
.filter(
(p) =>
p.jsDocTags.find(
(tag) => tag.name === "hidden" || tag.name === "internal",
) === undefined &&
(props.filter === undefined || props.filter(p) === true),
)
.map((r) =>
collectFunction({
version: props.version,
name: r.key.getSoleLiteral()!,
function: Metadata.unalias(r.value).functions[0]!,
description: r.description,
jsDocTags: r.jsDocTags,
collect,
}),
);
const { components, schemas } = JsonSchemasProgrammer.write({
version: props.version,
metadatas: definitions,
});
schemas.forEach((s, i) =>
setters[i]?.(s as __IJsonApplication.Schema<Version>),
);
return {
version: props.version,
components: components as any,
functions,
};
};
export const writeDescription = <Kind extends "summary" | "title">(props: {
description: string | null;
jsDocTags: IJsDocTagInfo[];
kind: Kind;
}): Kind extends "summary"
? { summary?: string; description?: string }
: { title?: string; description?: string } => {
const title: string | undefined = (() => {
const [explicit] = getJsDocTexts({
jsDocTags: props.jsDocTags,
name: props.kind,
});
if (explicit?.length) return explicit;
else if (!props.description?.length) return undefined;
const index: number = props.description.indexOf("\n");
const top: string = (
index === -1 ? props.description : props.description.substring(0, index)
).trim();
return top.endsWith(".") ? top.substring(0, top.length - 1) : undefined;
})();
return {
[props.kind]: title,
description: props.description?.length ? props.description : undefined,
} as any;
};
const collectFunction = <Version extends "3.0" | "3.1">(props: {
version: Version;
name: string;
function: MetadataFunction;
description: string | null;
jsDocTags: IJsDocTagInfo[];
collect: (
metadata: Metadata,
setter: (schema: __IJsonApplication.Schema<Version>) => void,
) => void;
}): __IJsonApplication.IFunction<__IJsonApplication.Schema<Version>> => {
const deprecated: boolean = props.jsDocTags.some(
(tag) => tag.name === "deprecated",
);
const tags: string[] = props.jsDocTags
.map((tag) =>
tag.name === "tag"
? (tag.text?.filter((elem) => elem.kind === "text") ?? [])
: [],
)
.flat()
.map((elem) => elem.text)
.map((str) => str.trim().split(" ")[0] ?? "")
.filter((str) => !!str.length);
return {
name: props.name,
async: props.function.async,
parameters: props.function.parameters.map((param) => {
const appParam: __IJsonApplication.IParameter<
__IJsonApplication.Schema<Version>
> = {
name: param.name,
...writeDescription({
description:
param.description ??
param.jsDocTags.find((tag) => tag.name === "description")
?.text?.[0]?.text ??
props.jsDocTags
.find(
(tag) =>
tag.name === "param" && tag.text?.[0]?.text === param.name,
)
?.text?.map((e) => e.text)
.join("")
.substring(param.name.length) ??
null,
jsDocTags: props.jsDocTags,
kind: "title",
}),
required: param.type.isRequired(),
schema: null!,
};
props.collect(param.type, (schema) => (appParam.schema = schema));
return appParam;
}),
output: props.function.output.size()
? (() => {
const appOutput: __IJsonApplication.IOutput<
__IJsonApplication.Schema<Version>
> = {
schema: null!,
required: props.function.output.isRequired(),
description:
writeDescriptionFromJsDocTag({
jsDocTags: props.jsDocTags,
name: "return",
}) ??
writeDescriptionFromJsDocTag({
jsDocTags: props.jsDocTags,
name: "returns",
}) ??
undefined,
};
props.collect(
props.function.output,
(schema) => (appOutput.schema = schema),
);
return appOutput;
})()
: undefined,
description: props.description ?? undefined,
deprecated: deprecated || undefined,
tags: tags.length ? tags : undefined,
};
};
}
const writeDescriptionFromJsDocTag = (props: {
jsDocTags: IJsDocTagInfo[];
name: string;
parameter?: string;
}): string | null => {
const parametric: (elem: IJsDocTagInfo) => boolean = props.parameter
? (tag) =>
tag.text!.find(
(elem) =>
elem.kind === "parameterName" && elem.text === props.parameter,
) !== undefined
: () => true;
const tag: IJsDocTagInfo | undefined = props.jsDocTags.find(
(tag) => tag.name === props.name && tag.text && parametric(tag),
);
return tag && tag.text
? (tag.text.find((elem) => elem.kind === "text")?.text ?? null)
: null;
};
const getJsDocTexts = (props: {
jsDocTags: IJsDocTagInfo[];
name: string;
}): string[] =>
props.jsDocTags
.filter(
(tag) =>
tag.name === props.name &&
tag.text &&
tag.text.find((elem) => elem.kind === "text" && elem.text.length) !==
undefined,
)
.map((tag) => tag.text!.find((elem) => elem.kind === "text")!.text);