@nestia/sdk
Version:
Nestia SDK and Swagger generator
378 lines (355 loc) • 13.9 kB
text/typescript
import ts from "typescript";
import { IJsDocTagInfo } from "typia";
import { ExpressionFactory } from "typia/lib/factories/ExpressionFactory";
import { TypeFactory } from "typia/lib/factories/TypeFactory";
import { IMetadataTypeTag } from "typia/lib/schemas/metadata/IMetadataTypeTag";
import { Metadata } from "typia/lib/schemas/metadata/Metadata";
import { MetadataAliasType } from "typia/lib/schemas/metadata/MetadataAliasType";
import { MetadataArray } from "typia/lib/schemas/metadata/MetadataArray";
import { MetadataAtomic } from "typia/lib/schemas/metadata/MetadataAtomic";
import { MetadataConstantValue } from "typia/lib/schemas/metadata/MetadataConstantValue";
import { MetadataEscaped } from "typia/lib/schemas/metadata/MetadataEscaped";
import { MetadataObjectType } from "typia/lib/schemas/metadata/MetadataObjectType";
import { MetadataProperty } from "typia/lib/schemas/metadata/MetadataProperty";
import { MetadataTuple } from "typia/lib/schemas/metadata/MetadataTuple";
import { Escaper } from "typia/lib/utils/Escaper";
import { INestiaProject } from "../../structures/INestiaProject";
import { FilePrinter } from "./FilePrinter";
import { ImportDictionary } from "./ImportDictionary";
import { SdkTypeTagProgrammer } from "./SdkTypeTagProgrammer";
export namespace SdkTypeProgrammer {
/* -----------------------------------------------------------
FACADE
----------------------------------------------------------- */
export const write =
(project: INestiaProject) =>
(importer: ImportDictionary) =>
(meta: Metadata, parentEscaped: boolean = false): ts.TypeNode => {
const union: ts.TypeNode[] = [];
// COALESCES
if (meta.any) union.push(TypeFactory.keyword("any"));
if (meta.nullable) union.push(writeNode("null"));
if (meta.isRequired() === false) union.push(writeNode("undefined"));
if (parentEscaped === false && meta.escaped)
union.push(write_escaped(project)(importer)(meta.escaped));
// ATOMIC TYPES
for (const c of meta.constants)
for (const value of c.values) union.push(write_constant(value));
for (const tpl of meta.templates)
union.push(write_template(project)(importer)(tpl.row ?? tpl));
for (const atom of meta.atomics) union.push(write_atomic(importer)(atom));
// OBJECT TYPES
for (const tuple of meta.tuples)
union.push(write_tuple(project)(importer)(tuple));
for (const array of meta.arrays)
union.push(write_array(project)(importer)(array));
for (const object of meta.objects)
if (
object.type.name === "object" ||
object.type.name === "__type" ||
object.type.name.startsWith("__type.") ||
object.type.name === "__object" ||
object.type.name.startsWith("__object.")
)
union.push(write_object(project)(importer)(object.type));
else union.push(write_alias(project)(importer)(object.type));
for (const alias of meta.aliases)
union.push(write_alias(project)(importer)(alias.type));
for (const native of meta.natives)
if (native.name === "Blob" || native.name === "File")
union.push(write_native(native.name));
return union.length === 1
? union[0]
: ts.factory.createUnionTypeNode(union);
};
export const write_object =
(project: INestiaProject) =>
(importer: ImportDictionary) =>
(object: MetadataObjectType): ts.TypeNode => {
const regular = object.properties.filter((p) => p.key.isSoleLiteral());
const dynamic = object.properties.filter((p) => !p.key.isSoleLiteral());
return FilePrinter.description(
regular.length && dynamic.length
? ts.factory.createIntersectionTypeNode([
write_regular_property(project)(importer)(regular),
...dynamic.map(write_dynamic_property(project)(importer)),
])
: dynamic.length
? ts.factory.createIntersectionTypeNode(
dynamic.map(write_dynamic_property(project)(importer)),
)
: write_regular_property(project)(importer)(regular),
writeComment([])(object.description ?? null, object.jsDocTags),
);
};
const write_escaped =
(project: INestiaProject) =>
(importer: ImportDictionary) =>
(meta: MetadataEscaped): ts.TypeNode => {
if (
meta.original.size() === 1 &&
meta.original.natives.length === 1 &&
meta.original.natives[0].name === "Date"
)
return ts.factory.createIntersectionTypeNode([
TypeFactory.keyword("string"),
SdkTypeTagProgrammer.write(importer, "string", {
name: "Format",
value: "date-time",
} as IMetadataTypeTag),
]);
return write(project)(importer)(meta.returns, true);
};
/* -----------------------------------------------------------
ATOMICS
----------------------------------------------------------- */
const write_constant = (value: MetadataConstantValue) => {
if (typeof value.value === "boolean")
return ts.factory.createLiteralTypeNode(
value ? ts.factory.createTrue() : ts.factory.createFalse(),
);
else if (typeof value.value === "bigint")
return ts.factory.createLiteralTypeNode(
value.value < BigInt(0)
? ts.factory.createPrefixUnaryExpression(
ts.SyntaxKind.MinusToken,
ts.factory.createBigIntLiteral((-value).toString()),
)
: ts.factory.createBigIntLiteral(value.toString()),
);
else if (typeof value.value === "number")
return ts.factory.createLiteralTypeNode(
ExpressionFactory.number(value.value),
);
return ts.factory.createLiteralTypeNode(
ts.factory.createStringLiteral(value.value as string),
);
};
const write_template =
(project: INestiaProject) =>
(importer: ImportDictionary) =>
(meta: Metadata[]): ts.TypeNode => {
const head: boolean = meta[0].isSoleLiteral();
const spans: [ts.TypeNode | null, string | null][] = [];
for (const elem of meta.slice(head ? 1 : 0)) {
const last =
spans.at(-1) ??
(() => {
const tuple = [null!, null!] as [ts.TypeNode | null, string | null];
spans.push(tuple);
return tuple;
})();
if (elem.isSoleLiteral())
if (last[1] === null)
last[1] = String(elem.constants[0].values[0].value);
else
spans.push([
ts.factory.createLiteralTypeNode(
ts.factory.createStringLiteral(
String(elem.constants[0].values[0].value),
),
),
null,
]);
else if (last[0] === null) last[0] = write(project)(importer)(elem);
else spans.push([write(project)(importer)(elem), null]);
}
return ts.factory.createTemplateLiteralType(
ts.factory.createTemplateHead(
head ? (meta[0].constants[0].values[0].value as string) : "",
),
spans
.filter(([node]) => node !== null)
.map(([node, str], i, array) =>
ts.factory.createTemplateLiteralTypeSpan(
node!,
(i !== array.length - 1
? ts.factory.createTemplateMiddle
: ts.factory.createTemplateTail)(str ?? ""),
),
),
);
};
const write_atomic =
(importer: ImportDictionary) =>
(meta: MetadataAtomic): ts.TypeNode =>
write_type_tag_matrix(importer)(
meta.type as "boolean" | "bigint" | "number" | "string",
ts.factory.createKeywordTypeNode(
meta.type === "boolean"
? ts.SyntaxKind.BooleanKeyword
: meta.type === "bigint"
? ts.SyntaxKind.BigIntKeyword
: meta.type === "number"
? ts.SyntaxKind.NumberKeyword
: ts.SyntaxKind.StringKeyword,
),
meta.tags,
);
/* -----------------------------------------------------------
INSTANCES
----------------------------------------------------------- */
const write_array =
(project: INestiaProject) =>
(importer: ImportDictionary) =>
(meta: MetadataArray): ts.TypeNode =>
write_type_tag_matrix(importer)(
"array",
ts.factory.createArrayTypeNode(
write(project)(importer)(meta.type.value),
),
meta.tags,
);
const write_tuple =
(project: INestiaProject) =>
(importer: ImportDictionary) =>
(meta: MetadataTuple): ts.TypeNode =>
ts.factory.createTupleTypeNode(
meta.type.elements.map((elem) =>
elem.rest
? ts.factory.createRestTypeNode(
ts.factory.createArrayTypeNode(
write(project)(importer)(elem.rest),
),
)
: elem.optional
? ts.factory.createOptionalTypeNode(
write(project)(importer)(elem),
)
: write(project)(importer)(elem),
),
);
const write_regular_property =
(project: INestiaProject) =>
(importer: ImportDictionary) =>
(properties: MetadataProperty[]): ts.TypeLiteralNode =>
ts.factory.createTypeLiteralNode(
properties.map((p) =>
FilePrinter.description(
ts.factory.createPropertySignature(
undefined,
Escaper.variable(String(p.key.constants[0].values[0].value))
? ts.factory.createIdentifier(
String(p.key.constants[0].values[0].value),
)
: ts.factory.createStringLiteral(
String(p.key.constants[0].values[0].value),
),
p.value.isRequired() === false
? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
: undefined,
SdkTypeProgrammer.write(project)(importer)(p.value),
),
writeComment(p.value.atomics)(p.description, p.jsDocTags),
),
),
);
const write_dynamic_property =
(project: INestiaProject) =>
(importer: ImportDictionary) =>
(property: MetadataProperty): ts.TypeLiteralNode =>
ts.factory.createTypeLiteralNode([
FilePrinter.description(
ts.factory.createIndexSignature(
undefined,
[
ts.factory.createParameterDeclaration(
undefined,
undefined,
ts.factory.createIdentifier("key"),
undefined,
SdkTypeProgrammer.write(project)(importer)(property.key),
),
],
SdkTypeProgrammer.write(project)(importer)(property.value),
),
writeComment(property.value.atomics)(
property.description,
property.jsDocTags,
),
),
]);
const write_alias =
(project: INestiaProject) =>
(importer: ImportDictionary) =>
(meta: MetadataAliasType | MetadataObjectType): ts.TypeNode => {
importInternalFile(project)(importer)(meta.name);
return ts.factory.createTypeReferenceNode(meta.name);
};
const write_native = (name: string): ts.TypeNode =>
ts.factory.createTypeReferenceNode(name);
/* -----------------------------------------------------------
MISCELLANEOUS
----------------------------------------------------------- */
const write_type_tag_matrix =
(importer: ImportDictionary) =>
(
from: "array" | "boolean" | "number" | "bigint" | "string" | "object",
base: ts.TypeNode,
matrix: IMetadataTypeTag[][],
): ts.TypeNode => {
matrix = matrix.filter((row) => row.length !== 0);
if (matrix.length === 0) return base;
else if (matrix.length === 1)
return ts.factory.createIntersectionTypeNode([
base,
...matrix[0].map((tag) =>
SdkTypeTagProgrammer.write(importer, from, tag),
),
]);
return ts.factory.createIntersectionTypeNode([
base,
ts.factory.createUnionTypeNode(
matrix.map((row) =>
row.length === 1
? SdkTypeTagProgrammer.write(importer, from, row[0])
: ts.factory.createIntersectionTypeNode(
row.map((tag) =>
SdkTypeTagProgrammer.write(importer, from, tag),
),
),
),
),
]);
};
}
const writeNode = (text: string) => ts.factory.createTypeReferenceNode(text);
const writeComment =
(atomics: MetadataAtomic[]) =>
(description: string | null, jsDocTags: IJsDocTagInfo[]): string => {
const lines: string[] = [];
if (description?.length)
lines.push(...description.split("\n").map((s) => `${s}`));
const filtered: IJsDocTagInfo[] =
!!atomics.length && !!jsDocTags?.length
? jsDocTags.filter(
(tag) =>
!atomics.some((a) =>
a.tags.some((r) => r.some((t) => t.kind === tag.name)),
),
)
: (jsDocTags ?? []);
if (description?.length && filtered.length) lines.push("");
if (filtered.length)
lines.push(
...filtered.map((t) =>
t.text?.length
? `@${t.name} ${t.text.map((e) => e.text).join("")}`
: `@${t.name}`,
),
);
return lines.join("\n");
};
const importInternalFile =
(project: INestiaProject) =>
(importer: ImportDictionary) =>
(name: string) => {
const top = name.split(".")[0];
if (importer.file === `${project.config.output}/structures/${top}.ts`)
return;
importer.internal({
type: true,
file: `${project.config.output}/structures/${name.split(".")[0]}`,
instance: top,
});
};