genezio
Version:
Command line utility to interact with Genezio infrastructure.
445 lines (426 loc) • 15.6 kB
JavaScript
import Mustache from "mustache";
import { AstNodeType, } from "../../models/genezioModels.js";
import { TriggerType } from "../../projectConfiguration/yaml/models.js";
import { dartSdk } from "../templates/dartSdk.js";
import { castArrayRecursivelyInitial, castMapRecursivelyInitial, getParamType, } from "../../utils/dartAstCasting.js";
import path from "path";
// https://dart.dev/language/keywords
const DART_RESERVED_WORDS = [
"abstract",
"as",
"assert",
"async",
"await",
"break",
"case",
"catch",
"class",
"const",
"continue",
"covariant",
"default",
"deferred",
"do",
"dynamic",
"else",
"enum",
"export",
"extends",
"extension",
"external",
"factory",
"false",
"final",
"finally",
"for",
"Function",
"get",
"hide",
"if",
"implements",
"import",
"in",
"interface",
"is",
"library",
"mixin",
"new",
"null",
"on",
"operator",
"part",
"rethrow",
"return",
"set",
"show",
"static",
"super",
"switch",
"sync",
"this",
"throw",
"true",
"try",
"typedef",
"var",
"void",
"while",
"with",
"yield",
];
/**
* Template that is going to be used for models.
*/
const modelTemplate = `
import './models.dart';
{{#classes}}
class {{name}} {
{{#fields}}
{{{type}}} {{fieldName}};
{{/fields}}
{{name}}({{#fields}}this.{{fieldName}},{{/fields}});
{{{fromJson}}}
{{{toJson}}}
}
{{/classes}}
`;
/**
* Template that is going to be used for main class.
*/
const template = `/// This is an auto generated code. This code should not be modified since the file can be overwritten
/// if new genezio commands are executed.
import 'remote.dart';
import './models/models.dart';
{{#imports}}
import '{{{name}}}';
{{/imports}}
{{#otherClasses}}
class {{name}} {
{{#fields}}
{{{type}}} {{fieldName}};
{{/fields}}
{{name}}({{#fields}}this.{{fieldName}},{{/fields}});
{{{fromJson}}}
{{{toJson}}}
}
{{/otherClasses}}
{{#classDocLines.length}}
/**
{{#classDocLines}}
* {{{.}}}
{{/classDocLines}}
*/
{{/classDocLines.length}}
class {{{className}}} {
static final remote = Remote("{{{_url}}}");
{{#methods}}
{{#methodDocLines.length}}
/**
{{#methodDocLines}}
* {{{.}}}
{{/methodDocLines}}
*/
{{/methodDocLines.length}}
static {{{returnType}}} {{{name}}}({{#parameters}}{{{name}}}{{^last}}, {{/last}}{{/parameters}}) async {
{{#returnTypeCast}}
final response = await remote.call({{{methodCaller}}}, [{{#sendParameters}}{{{name}}}{{^last}}, {{/last}}{{/sendParameters}}]);
{{/returnTypeCast}}
{{^returnTypeCast}}
await remote.call({{{methodCaller}}}, [{{#sendParameters}}{{{name}}}{{^last}}, {{/last}}{{/sendParameters}}]);
{{/returnTypeCast}}
{{#returnTypeCast}}
return {{{returnTypeCast}}};
{{/returnTypeCast}}
}
{{/methods}}
}
`;
class SdkGenerator {
async generateSdk(sdkGeneratorInput) {
const generateSdkOutput = {
files: [],
};
// A dictionary where the key is the path of the file and the value is the view object.
// This dictionary is used to create and aggregate the model views.
const modelClassesView = {};
for (const classInfo of sdkGeneratorInput.classesInfo) {
const _url = "%%%link_to_be_replace%%%";
const classConfiguration = classInfo.classConfiguration;
let classDefinition = undefined;
if (classInfo.program.body === undefined) {
continue;
}
const view = {
otherClasses: [],
className: undefined,
_url: _url,
methods: [],
imports: [],
classDocLines: [],
};
const mainClass = classInfo.program.body.find((e) => e.type === AstNodeType.ClassDefinition);
for (const elem of classInfo.program.body) {
if (elem.type === AstNodeType.ClassDefinition) {
classDefinition = elem;
(view.classDocLines =
classDefinition.docString?.replace(/\n+$/, "").split("\n") || []),
this.populateViewForMainClass(classDefinition, classConfiguration, view);
}
else if (elem.type === AstNodeType.StructLiteral) {
const structLiteral = elem;
const fromJson = this.generateFromJsonImplementationForClass(structLiteral);
const toJson = this.generateToJsonImplementationForClass(structLiteral);
// If the model belongs to the class, add it to the class view.
// Otherwise, we should add it to its proper model view.
if (mainClass?.path === elem.path) {
view.otherClasses.push({
name: structLiteral.name,
fields: structLiteral.typeLiteral.properties.map((e) => ({
type: getParamType(e.type),
fieldName: e.name,
})),
fromJson: fromJson,
toJson: toJson,
});
}
else {
this.addModelToModelViews(modelClassesView, structLiteral, fromJson, toJson);
}
}
}
if (classDefinition === undefined) {
continue;
}
const rawSdkClassName = `${classDefinition.name}.dart`;
const sdkClassName = rawSdkClassName
.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
.slice(1);
generateSdkOutput.files.push({
path: sdkClassName,
data: Mustache.render(template, view),
className: classDefinition.name,
});
}
// Generate the models files
generateSdkOutput.files.push(...this.getModelFiles(modelClassesView));
// Generate the remote.js file
generateSdkOutput.files.push({
className: "Remote",
path: "remote.dart",
data: dartSdk,
});
return generateSdkOutput;
}
populateViewForMainClass(classDefinition, classConfiguration, view) {
view.className = classDefinition.name;
for (const methodDefinition of classDefinition.methods) {
const methodConfiguration = classConfiguration.methods.find((e) => e.name === methodDefinition.name);
const methodConfigurationType = methodConfiguration?.type || classConfiguration.type;
if (methodConfigurationType !== TriggerType.jsonrpc ||
classConfiguration.type !== TriggerType.jsonrpc) {
continue;
}
const methodView = {
name: methodDefinition.name,
parameters: [],
methodDocLines: methodDefinition.docString?.replace(/\n+$/, "").split("\n") || [],
methodCaller: methodDefinition.params.length === 0
? `"${classDefinition.name}.${methodDefinition.name}"`
: `"${classDefinition.name}.${methodDefinition.name}"`,
};
if (methodDefinition.returnType.type === AstNodeType.VoidLiteral) {
methodView.returnType = this.getReturnType(methodDefinition.returnType);
methodView.returnTypeCast = undefined;
}
else {
methodView.returnTypeCast = this.castReturnTypeToPropertyType(methodDefinition.returnType, "response");
methodView.returnType = this.getReturnType(methodDefinition.returnType);
}
methodView.parameters = methodDefinition.params.map((e) => {
return {
name: getParamType(e.paramType) +
" " +
(DART_RESERVED_WORDS.includes(e.name) ? e.name + "_" : e.name),
last: false,
};
});
if (methodView.parameters.length > 0) {
methodView.parameters[methodView.parameters.length - 1].last = true;
}
methodView.sendParameters = methodDefinition.params.map((e) => {
return {
name: DART_RESERVED_WORDS.includes(e.name) ? e.name + "_" : e.name,
last: false,
};
});
if (methodView.sendParameters.length > 0) {
methodView.sendParameters[methodView.parameters.length - 1].last = true;
}
view.methods.push(methodView);
}
}
getModelFiles(modelClassesView) {
const files = [];
Object.entries(modelClassesView).forEach(([key, value]) => {
files.push({
className: "",
path: path.join("models", decodeURIComponent(key.split("/").pop())),
data: Mustache.render(modelTemplate, value),
});
});
let exportModelsContent = "";
Object.keys(modelClassesView).forEach((key) => {
exportModelsContent += `export './${decodeURIComponent(key.split("/").pop())}';\n`;
});
files.push({
className: "",
path: path.join("models", "models.dart"),
data: exportModelsContent,
});
return files;
}
/**
* Create a model and add it to the moustache model views.
*
* If the model already exists, it will not be added.
*
* @param views Moustache views array with models where the new model will be added.
* @param struct The struct from which the view object will be created and added to the views array.
* @param fromJson
* @param toJson
*/
addModelToModelViews(views, struct, fromJson, toJson) {
if (!views[struct.path]) {
views[struct.path] = {
classes: [],
};
}
if (!views[struct.path].classes.find((e) => e.name === struct.name)) {
views[struct.path] = {
classes: [
...views[struct.path].classes,
{
name: struct.name,
fields: struct.typeLiteral.properties.map((e) => ({
type: getParamType(e.type),
fieldName: e.name,
})),
fromJson: fromJson,
toJson: toJson,
},
],
};
}
}
generateFromJsonImplementationForClass(elem) {
let implementation = "";
implementation += `factory ${elem.name}.fromJson(Map<String, dynamic> json) => `;
implementation += `${elem.name}(`;
elem.typeLiteral.properties.forEach((e) => {
implementation += this.generateFromJsonImplementation(e.type, e.name);
});
implementation += `);`;
return implementation;
}
generateToJsonImplementationForClass(elem) {
let implementation = "Map<String, dynamic> toJson() => <String, dynamic>{";
elem.typeLiteral.properties.forEach((e) => {
implementation += `"${e.name}": ${e.name},`;
});
implementation += "};";
return implementation;
}
castReturnTypeToPropertyType(node, variableName) {
let implementation = "";
switch (node.type) {
case AstNodeType.StringLiteral:
implementation += `${variableName} as String`;
break;
case AstNodeType.DoubleLiteral:
implementation += `${variableName} as double`;
break;
case AstNodeType.BooleanLiteral:
implementation += `${variableName} as bool`;
break;
case AstNodeType.IntegerLiteral:
implementation += `${variableName} as int`;
break;
case AstNodeType.PromiseType:
implementation += this.castReturnTypeToPropertyType(node.generic, variableName);
break;
case AstNodeType.CustomNodeLiteral:
implementation += `${node.rawValue}.fromJson(${variableName} as Map<String, dynamic>)`;
break;
case AstNodeType.ArrayType:
implementation += castArrayRecursivelyInitial(node, variableName);
break;
case AstNodeType.MapType:
implementation += castMapRecursivelyInitial(node, variableName);
break;
case AstNodeType.DateType:
implementation += `${variableName} as DateTime`;
}
return implementation;
}
generateFromJsonImplementation(node, name) {
let implementation = "";
switch (node.type) {
case AstNodeType.StringLiteral:
implementation += `json['${name}'] as String,`;
break;
case AstNodeType.DoubleLiteral:
implementation += `json['${name}'] as double,`;
break;
case AstNodeType.BooleanLiteral:
implementation += `json['${name}'] as bool,`;
break;
case AstNodeType.IntegerLiteral:
implementation += `json['${name}'] as int,`;
break;
case AstNodeType.CustomNodeLiteral:
implementation += `${node.rawValue}.fromJson(json['${name}'] as Map<String, dynamic>),`;
break;
case AstNodeType.ArrayType:
implementation +=
castArrayRecursivelyInitial(node, `json['${name}']`) + ",";
break;
case AstNodeType.MapType:
implementation +=
castMapRecursivelyInitial(node, `json['${name}']`) + ",";
break;
case AstNodeType.DateType:
implementation += `DateTime.parse(json['${name}'].toString()),`;
}
return implementation;
}
getReturnType(elem) {
switch (elem.type) {
case AstNodeType.StringLiteral:
return "Future<String>";
case AstNodeType.DoubleLiteral:
return "Future<double>";
case AstNodeType.BooleanLiteral:
return "Future<bool>";
case AstNodeType.IntegerLiteral:
return "Future<int>";
case AstNodeType.AnyLiteral:
return "Future<Object>";
case AstNodeType.VoidLiteral:
return "Future<void>";
case AstNodeType.PromiseType:
return `Future<${getParamType(elem.generic)}>`;
case AstNodeType.ArrayType:
return `Future<List<${getParamType(elem.generic)}>>`;
case AstNodeType.MapType:
return `Future<Map<${getParamType(elem.genericKey)}, ${getParamType(elem.genericValue)}>>`;
case AstNodeType.CustomNodeLiteral:
return `Future<${elem.rawValue}>`;
case AstNodeType.DateType:
return "Future<DateTime>";
default:
return "Future<Object>";
}
}
}
const supportedLanguages = ["dart"];
export default { SdkGenerator, supportedLanguages };