@udraft/core
Version:
uDraft is a language and stack agnostic code-generation tool that simplifies full-stack development by converting a single YAML file into code for rapid development.
268 lines (229 loc) • 9.18 kB
text/typescript
import * as Case from "case";
import * as path from "path";
import { UModel } from "../entities/model";
import { URenderer } from "../entities/renderer";
import { $attr } from "../shortcuts/queries";
import { RenderContent, RenderPath, RenderSelection } from "../types/renderer";
import { closeCursor, writeToCursor } from "../utils/rendering";
import {
_array,
_enum,
_ref,
_required,
_rootModule,
} from "../shortcuts/attributes";
import { UModule } from "../entities/module";
import { UField } from "../entities/field";
export default class TSClassRenderer extends URenderer {
private _entityDir = "src/entities";
private _dtoDir = "src/dtos";
private _enumDir = "src/types";
private _includeModuleInDir = true;
private _where?: (module: UModule, model: UModel) => boolean;
constructor(options?: {
modelDir?: string;
dtoDir?: string;
includeModuleInDir?: boolean;
where?: (module: UModule, model: UModel) => boolean;
}) {
super("ts@classes");
if (options?.modelDir) this._entityDir = options.modelDir;
if (options?.dtoDir) this._dtoDir = options.dtoDir;
if (options?.includeModuleInDir)
this._includeModuleInDir = options.includeModuleInDir;
if (options?.where) this._where = options.where;
}
$isDto(model: UModel) {
const output = this.$output(model.$name());
if (output) return !!output.meta?.isDto;
return null;
}
$resolveImport(from: string, model: UModel): string {
const modelPath = this.$path(this.$className(model));
if (!modelPath?.path) return "";
const importPath = this.$resolveRelativePath(from, modelPath.path);
return `import { ${this.$className(model)} } from '${importPath}';\n`;
}
$key(model: UModel) {
return this.$className(model);
}
$keys(models: UModel[]) {
return models.map((model) => this.$key(model));
}
$paths(models?: UModel[]) {
let paths = super.$paths();
if (models)
paths = paths.filter((p) => models.some((m) => p.key === this.$key(m)));
return paths;
}
$className(model: UModel) {
return Case.pascal(model.$name());
}
$fileName(model: UModel, extension = true) {
return `${Case.kebab(model.$name())}${extension ? ".ts" : ""}`;
}
$fieldName(field: UField) {
let nameParts = field.$name().match(/([^A-Za-z]*)(.+)/);
if (nameParts) return nameParts[1] + Case.camel(nameParts[2]);
return Case.camel(field.$name());
}
$fieldType(field: UField) {
let type = field.$type() + "";
if (type === "date") type = "Date";
else if (type === "reference") type = "string";
else if (["int", "float"].includes(type)) type = "number";
return type;
}
$fieldSignature(field: UField) {
let type = this.$fieldType(field);
if (type === "nested") {
const nestedModel = $attr(field, _ref());
if (nestedModel) type = this.$className(nestedModel);
}
return `${this.$fieldName(field)}${
!$attr(field, _required()) ? "?" : ""
}: ${type}${$attr(field, _array()) ? "[]" : ""}`;
}
async select(): Promise<RenderSelection> {
const models = this.$models(this._where);
const paths: RenderPath[] = [];
models.forEach((model) => {
if (paths.some((p) => p.key === this.$key(model))) return;
const mod = $attr(model, _rootModule());
const isDto = !!model.$name().match(/dto$/i);
const isEnum = !!$attr(model, _enum());
paths.push({
key: this.$key(model),
meta: { isDto, isEnum },
path: path.join(
isDto ? this._dtoDir : isEnum ? this._enumDir : this._entityDir,
this._includeModuleInDir ? Case.kebab(mod ? mod.$name() : "") : "",
this.$fileName(model)
),
});
});
return {
paths,
models,
};
}
async render(): Promise<RenderContent[]> {
const output: RenderContent[] = [];
const models = this.$selection().models || [];
models.forEach((model) => {
const modelKey = this.$key(model);
const modelPath = this.$path(modelKey);
if (!modelPath) return;
let content = "";
const enumDefinition = $attr(model, _enum());
if (enumDefinition) {
const enumCursor = "#enum-cursor\n";
content = `export enum ${this.$className(model)} {\n${enumCursor}}`;
Object.keys(enumDefinition).forEach((key) => {
content = writeToCursor(
enumCursor,
` ${key} = ${JSON.stringify(enumDefinition[key])},\n`,
content
);
});
content = closeCursor(enumCursor, content);
} else {
const fieldCursor = "#field-cursor\n";
const importCursor = "#import-cursor\n";
const fromJsonCursor = "#from-json-cursor\n";
const toJsonCursor = "#to-json-cursor\n";
content = `${importCursor}export class ${this.$className(
model
)} {\n${fieldCursor}\n constructor(data?: Omit<${this.$className(
model
)}, "toJson">) {
if (data) ${this.$className(model)}.fromJson(data, this);
}\n\n${fromJsonCursor}\n${toJsonCursor}}`;
const fields = model.$fields();
const importedModels: string[] = [];
fields.forEach((field) => {
const fieldSignature = this.$fieldSignature(field);
if (field.$type() == "nested") {
const nestedModel = $attr(field, _ref());
if (nestedModel) {
if (
this.$className(model) != this.$className(nestedModel) &&
!importedModels.includes(this.$className(nestedModel))
) {
content = writeToCursor(
importCursor,
this.$resolveImport(modelPath.path ?? "", nestedModel),
content
);
importedModels.push(this.$className(nestedModel));
}
}
}
content = writeToCursor(
fieldCursor,
` ${fieldSignature};\n`,
content
);
});
let fromJsonMethod = ` static fromJson(json: Record<string, any>, instance?: ${this.$className(
model
)}): ${this.$className(
model
)} {\n instance = instance ?? new ${this.$className(model)}();\n`;
let toJsonMethod = ` toJson(): Record<string, any> {\n return {\n`;
fields.forEach((field) => {
const fieldName = this.$fieldName(field);
const type = field.$type();
if (type === "date") {
// For Date fields, assume the JSON value is an ISO string.
fromJsonMethod += ` if(json.${fieldName}) instance.${fieldName} = new Date(json.${fieldName});\n`;
toJsonMethod += ` ${fieldName}: this.${fieldName} ? this.${fieldName}.toISOString() : undefined,\n`;
} else if (type === "nested") {
// For nested objects, call the nested type's fromJson/toJson.
const nestedModel = $attr(field, _ref());
const isEnum = !nestedModel || !!$attr(nestedModel, _enum());
if (nestedModel && !isEnum) {
if ($attr(field, _array())) {
// Nested array: map each item.
fromJsonMethod += ` if(json.${fieldName}) instance.${fieldName} =json.${fieldName}.map((item: any) => ${this.$className(
nestedModel
)}.fromJson(item));\n`;
toJsonMethod += ` ${fieldName}: this.${fieldName} ? this.${fieldName}.map((item: any) => item.toJson()) : undefined,\n`;
} else {
// Single nested object.
fromJsonMethod += ` if(json.${fieldName}) instance.${fieldName} = ${this.$className(
nestedModel
)}.fromJson(json.${fieldName});\n`;
toJsonMethod += ` ${fieldName}: this.${fieldName} ? this.${fieldName}.toJson() : undefined,\n`;
}
} else if (nestedModel) {
fromJsonMethod += ` instance.${fieldName} = json.${fieldName};\n`;
toJsonMethod += ` ${fieldName}: this.${fieldName},\n`;
}
} else {
// For primitives and other types, assign directly.
fromJsonMethod += ` instance.${fieldName} = json.${fieldName};\n`;
toJsonMethod += ` ${fieldName}: this.${fieldName},\n`;
}
});
fromJsonMethod += ` return instance;\n }\n`;
toJsonMethod += ` };\n }\n`;
content = writeToCursor(fromJsonCursor, fromJsonMethod, content);
content = writeToCursor(toJsonCursor, toJsonMethod, content);
if (importedModels.length > 0)
content = writeToCursor(importCursor, "\n", content);
content = closeCursor(fieldCursor, content);
content = closeCursor(importCursor, content);
content = closeCursor(fromJsonCursor, content);
content = closeCursor(toJsonCursor, content);
}
if (content)
output.push({
key: modelKey,
content,
meta: modelPath.meta ?? {},
});
});
return output;
}
}