@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.
277 lines (239 loc) • 8.76 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 {
_array,
_enum,
_ref,
_required,
_rootModule,
} from "../shortcuts/attributes";
import { UModule } from "../entities/module";
import { UField } from "../entities/field";
export default class DartClassRenderer extends URenderer {
private _modelDir = "lib/models";
private _dtoDir = "lib/dtos";
private _enumDir = "lib/enums";
private _includeModuleInDir = true;
private _where?: (module: UModule, model: UModel) => boolean;
constructor(options?: {
modelDir?: string;
dtoDir?: string;
enumDir?: string;
includeModuleInDir?: boolean;
where?: (module: UModule, model: UModel) => boolean;
}) {
super("dart@classes");
if (options?.modelDir) this._modelDir = options.modelDir;
if (options?.dtoDir) this._dtoDir = options.dtoDir;
if (options?.enumDir) this._enumDir = options.enumDir;
if (options?.includeModuleInDir !== undefined)
this._includeModuleInDir = options.includeModuleInDir;
this._where = options?.where;
}
$isDto(model: UModel) {
const output = this.$output(model.$name());
return output?.meta?.isDto ?? false;
}
$resolveImport(from: string, model: UModel): string {
const modelPath = this.$path(this.$className(model));
if (!modelPath?.path) return "";
const fromDir = path.dirname(from);
const toDir = path.dirname(modelPath.path);
const relativePath = path.relative(fromDir, toDir);
const fileName = this.$fileName(model, false);
const importPath = path.join(relativePath, fileName);
const normalizedPath = importPath.split(path.sep).join("/");
return `import '${normalizedPath}.dart';\n`;
}
$key(model: UModel) {
return this.$className(model);
}
$className(model: UModel) {
return Case.pascal(model.$name());
}
$fileName(model: UModel, extension = true) {
return `${Case.snake(model.$name())}${extension ? ".dart" : ""}`;
}
$fieldName(field: UField) {
const nameParts = field.$name().match(/([^A-Za-z]+)(.+)/);
return nameParts
? nameParts[1] + Case.camel(nameParts[2])
: Case.camel(field.$name());
}
$fieldType(field: UField): string {
let type: string;
switch (field.$type()) {
case "date":
type = "DateTime";
break;
case "reference":
case "string":
type = "String";
break;
case "int":
type = "int";
break;
case "float":
type = "double";
break;
case "boolean":
type = "bool";
break;
case "nested": {
const nestedModel = $attr(field, _ref());
type = nestedModel ? this.$className(nestedModel) : "dynamic";
break;
}
default:
type = field.$type().toString();
}
if ($attr(field, _array())) {
type = `List<${type}>`;
}
if (!$attr(field, _required())) {
type += "?";
}
return type;
}
private toDartValue(value: any): string {
if (typeof value === "string") return `'${value.replace(/'/g, "\\'")}'`;
if (typeof value === "number") return value.toString();
if (typeof value === "boolean") return value ? "true" : "false";
return JSON.stringify(value);
}
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._modelDir,
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 || [];
for (const model of models) {
const modelKey = this.$key(model);
const modelPath = this.$path(modelKey);
if (!modelPath) continue;
let content = "";
const className = this.$className(model);
const enumDefinition = $attr(model, _enum());
if (enumDefinition) {
const entries = Object.keys(enumDefinition).map((key) => {
const entryName = Case.camel(key);
const value = enumDefinition[key];
return `${entryName}(${this.toDartValue(value)})`;
});
const firstValue = Object.values(enumDefinition)[0];
let valueType = "dynamic";
if (typeof firstValue === "string") {
valueType = "String";
} else if (typeof firstValue === "number") {
valueType = Number.isInteger(firstValue) ? "int" : "double";
} else if (typeof firstValue === "boolean") {
valueType = "bool";
}
content =
`enum ${className} {\n ${entries.join(",\n ")};\n\n` +
` final ${valueType} value;\n\n` +
` const ${className}(this.value);\n}`;
} else {
const fields = model.$fields();
let imports = "";
const importedModels: string[] = [];
let fieldDeclarations = "";
const constructorParams: string[] = [];
let fromJsonParams = "";
let toJsonAssignments = "";
fields.forEach((field) => {
const fieldName = this.$fieldName(field);
const fieldType = this.$fieldType(field);
const isRequired = $attr(field, _required());
// Handle nested models
if (field.$type() === "nested") {
const nestedModel = $attr(field, _ref());
if (nestedModel) {
const nestedClassName = this.$className(nestedModel);
if (!importedModels.includes(nestedClassName)) {
imports += this.$resolveImport(modelPath.path, nestedModel);
importedModels.push(nestedClassName);
}
}
}
// Build field declaration
fieldDeclarations += ` final ${fieldType} ${fieldName};\n`;
// Build constructor parameter
constructorParams.push(
` ${isRequired ? "required " : ""}this.${fieldName},`
);
// Build toJson and fromJson parameter
const refModel = $attr(field, _ref());
const isEnum = refModel ? !!$attr(refModel, _enum()) : false;
if (!isEnum && field.$type() === "nested") {
if ($attr(field, _array())) {
fromJsonParams += ` ${fieldName}: (json['${fieldName}'] as List<dynamic>)${
!isRequired ? "?" : ""
}.map((item) => ${this.$className(
$attr(field, _ref()) as UModel
)}.fromJson(item as Map<String, dynamic>))${
!isRequired ? "?" : ""
}.toList(),\n`;
toJsonAssignments += ` '${fieldName}': ${fieldName}${
!isRequired ? "?" : ""
}.map((item) => item.toJson())${
!isRequired ? "?" : ""
}.toList(),\n`;
} else {
fromJsonParams += ` ${fieldName}: ${this.$className(
$attr(field, _ref()) as UModel
)}.fromJson(json['${fieldName}']),\n`;
toJsonAssignments += ` '${fieldName}': ${fieldName}${
!isRequired ? "?" : ""
}.toJson(),\n`;
}
} else {
fromJsonParams += ` ${fieldName}: json['${fieldName}'],\n`;
toJsonAssignments += ` '${fieldName}': ${fieldName},\n`;
}
});
const constructor = constructorParams.length
? `\n ${className}({\n${constructorParams.join("\n")}\n });`
: "";
content =
`${imports}class ${className} {\n${fieldDeclarations}${constructor}\n\n` +
` factory ${className}.fromJson(Map<String, dynamic> json) {\n` +
` return ${className}(\n${fromJsonParams} );\n` +
` }\n\n` +
` Map<String, dynamic> toJson() {\n` +
` return {\n${toJsonAssignments} };\n` +
` }\n\n}`;
}
output.push({
key: modelKey,
content,
meta: modelPath.meta ?? {},
});
}
return output;
}
}