489 lines
13 KiB
TypeScript
489 lines
13 KiB
TypeScript
import {
|
|
TypeDefinition,
|
|
ServiceDefinition,
|
|
EnumDefinition,
|
|
TypeFieldDefinition,
|
|
Step,
|
|
} from "../ir";
|
|
|
|
import { CompileTarget } from "../compile";
|
|
// import * as prettier from "prettier";
|
|
|
|
type lineAppender = (ind: number, line: string | string[]) => void;
|
|
|
|
const conversion = {
|
|
boolean: "boolean",
|
|
number: "number",
|
|
string: "string",
|
|
};
|
|
|
|
function toJSType(type: string): string {
|
|
return (conversion as any)[type] || type;
|
|
}
|
|
|
|
export class TypescriptTarget extends CompileTarget {
|
|
name = "Typescript";
|
|
|
|
flavour: "esm" | "node" = "node";
|
|
|
|
start() {}
|
|
|
|
private generateImport(imports: string, path: string) {
|
|
return `import ${imports} from "${
|
|
path + (this.flavour === "esm" ? ".ts" : "")
|
|
}";\n`;
|
|
}
|
|
|
|
private generateImports(
|
|
a: lineAppender,
|
|
def: TypeDefinition | ServiceDefinition
|
|
) {
|
|
a(
|
|
0,
|
|
def.depends.map((dep) =>
|
|
this.generateImport(`${dep}, { verify_${dep} }`, "./" + dep)
|
|
)
|
|
);
|
|
}
|
|
|
|
private getFileName(typename: string) {
|
|
return typename + ".ts";
|
|
}
|
|
|
|
private writeFormattedFile(file: string, code: string) {
|
|
this.writeFile(file, code);
|
|
// const formatted = prettier.format(code, {
|
|
// parser: "typescript",
|
|
// tabWidth: 3,
|
|
// });
|
|
|
|
// this.writeFile(file, formatted);
|
|
}
|
|
|
|
generateType(def: TypeDefinition) {
|
|
let lines: string[] = [];
|
|
const a: lineAppender = (i, t) => {
|
|
if (!Array.isArray(t)) {
|
|
t = [t];
|
|
}
|
|
t.forEach((l) => lines.push(" ".repeat(i) + l.trim()));
|
|
};
|
|
|
|
this.generateImports(a, def);
|
|
a(0, `export default class ${def.name} {`);
|
|
a(
|
|
1,
|
|
def.fields.map((field) => {
|
|
let type = "";
|
|
if (field.array) {
|
|
type = toJSType(field.type) + "[]";
|
|
} else if (field.map) {
|
|
type = `Map<${toJSType(field.map)}, ${toJSType(field.type)}>`;
|
|
} else {
|
|
type = toJSType(field.type);
|
|
}
|
|
return `${field.name}?: ${type}; `;
|
|
})
|
|
);
|
|
|
|
a(0, ``);
|
|
a(1, `constructor(init?:Partial<${def.name}>){`);
|
|
a(2, `if(init){`);
|
|
def.fields.forEach((field) => {
|
|
a(3, `if(init["${field.name}"])`);
|
|
a(4, `this.${field.name} = init["${field.name}"];`);
|
|
});
|
|
a(2, `}`);
|
|
a(1, `}`);
|
|
|
|
a(0, ``);
|
|
|
|
a(0, ``);
|
|
|
|
a(1, `static verify(data: ${def.name}){`);
|
|
a(2, `return verify_${def.name}(data);`);
|
|
a(1, `}`);
|
|
a(0, `}`);
|
|
|
|
a(0, ``);
|
|
|
|
a(0, `export function verify_${def.name}(data: ${def.name}): boolean {`);
|
|
{
|
|
def.fields.forEach((field) => {
|
|
a(
|
|
1,
|
|
`if(data["${field.name}"] !== null && data["${field.name}"] !== undefined ) {`
|
|
);
|
|
|
|
const verifyType = (varName: string) => {
|
|
switch (field.type) {
|
|
case "string":
|
|
a(2, `if(typeof ${varName} !== "string") return false;`);
|
|
break;
|
|
case "number":
|
|
a(2, `if(typeof ${varName} !== "number") return false;`);
|
|
break;
|
|
case "boolean":
|
|
a(2, `if(typeof ${varName} !== "boolean") return false;`);
|
|
break;
|
|
default:
|
|
a(
|
|
2,
|
|
`if(!verify_${field.type}(${varName})) return false;`
|
|
);
|
|
}
|
|
};
|
|
|
|
if (field.array) {
|
|
a(2, `if(!Array.isArray(data["${field.name}"])) return false`);
|
|
a(2, `for(const elm of data["${field.name}"]) {`);
|
|
verifyType("elm");
|
|
a(2, `}`);
|
|
} else if (field.map) {
|
|
a(
|
|
2,
|
|
`if(typeof data["${field.name}"] !== "object") return false`
|
|
);
|
|
a(2, `for(const key in data["${field.name}"]) {`);
|
|
verifyType(`data["${field.name}"][key]`);
|
|
a(2, `}`);
|
|
} else {
|
|
verifyType(`data["${field.name}"]`);
|
|
}
|
|
a(1, "}");
|
|
a(0, ``);
|
|
});
|
|
a(1, `return true`);
|
|
}
|
|
a(0, `}`);
|
|
|
|
this.writeFormattedFile(this.getFileName(def.name), lines.join("\n"));
|
|
}
|
|
|
|
generateEnum(def: EnumDefinition) {
|
|
let lines: string[] = [];
|
|
const a: lineAppender = (i, t) => {
|
|
if (!Array.isArray(t)) {
|
|
t = [t];
|
|
}
|
|
t.forEach((l) => lines.push(" ".repeat(i) + l.trim()));
|
|
};
|
|
|
|
a(0, `enum ${def.name} {`);
|
|
for (const value of def.values) {
|
|
a(1, `${value.name}=${value.value},`);
|
|
}
|
|
a(0, `}`);
|
|
|
|
a(0, `export default ${def.name}`);
|
|
|
|
a(0, `export function verify_${def.name} (data: ${def.name}): boolean {`);
|
|
a(1, `return ${def.name}[data] != undefined`);
|
|
a(0, "}");
|
|
|
|
this.writeFormattedFile(this.getFileName(def.name), lines.join("\n"));
|
|
}
|
|
|
|
generateServiceClient(def: ServiceDefinition) {
|
|
this.writeFile(
|
|
"service_client.ts",
|
|
this.generateImport(
|
|
"{ RequestObject, ResponseObject, ErrorCodes, Logging }",
|
|
"./service_base"
|
|
) +
|
|
"\n\n" +
|
|
this.getTemplate("ts_service_client.ts")
|
|
);
|
|
|
|
|
|
let lines: string[] = [];
|
|
const a: lineAppender = (i, t) => {
|
|
if (!Array.isArray(t)) {
|
|
t = [t];
|
|
}
|
|
t.forEach((l) => lines.push(" ".repeat(i) + l.trim()));
|
|
};
|
|
|
|
this.generateImports(a, def);
|
|
|
|
a(0, `export type {`);
|
|
def.depends.forEach((dep) => {
|
|
a(1, `${dep},`);
|
|
});
|
|
a(0, `}`);
|
|
|
|
a(
|
|
0,
|
|
this.generateImport(
|
|
"{ verify_number, verify_string, verify_boolean }",
|
|
"./service_base"
|
|
)
|
|
);
|
|
|
|
a(
|
|
0,
|
|
this.generateImport(
|
|
"{ Service, ServiceProvider, getRandomID }",
|
|
"./service_client"
|
|
)
|
|
);
|
|
|
|
a(0, ``);
|
|
|
|
a(0, `export class ${def.name} extends Service {`);
|
|
a(1, `constructor(provider: ServiceProvider){`);
|
|
a(2, `super(provider, "${def.name}");`);
|
|
a(1, `}`);
|
|
|
|
for (const fnc of def.functions) {
|
|
const params = fnc.inputs
|
|
.map((e) => `${e.name}: ${toJSType(e.type)}`)
|
|
.join(",");
|
|
//TODO: Prio 2 : Add optional parameters to this and the declaration file
|
|
if (!fnc.return) {
|
|
a(1, `${fnc.name}(${params}): void {`);
|
|
a(2, `this._provider.sendMessage({`);
|
|
a(3, `jsonrpc: "2.0",`);
|
|
a(3, `method: "${def.name}.${fnc.name}",`);
|
|
a(3, `params: [...arguments]`);
|
|
a(2, `});`);
|
|
a(1, `}`);
|
|
} else {
|
|
const retType = fnc.return ? toJSType(fnc.return) : "void";
|
|
a(1, `${fnc.name}(${params}): Promise<${retType}> {`);
|
|
a(2, `return new Promise<${retType}>((ok, err) => {`);
|
|
a(3, `this._provider.sendMessage({`);
|
|
a(4, `jsonrpc: "2.0",`);
|
|
a(4, `id: getRandomID(16),`);
|
|
a(4, `method: "${def.name}.${fnc.name}",`);
|
|
a(4, `params: [...arguments]`);
|
|
a(3, `}, {`);
|
|
a(4, `ok, err`);
|
|
a(3, `});`);
|
|
a(2, `}).then(result => {`);
|
|
a(
|
|
3,
|
|
`if(!verify_${fnc.return}(result)) throw new Error("Invalid result data!");`
|
|
);
|
|
a(3, `return result;`);
|
|
a(2, `});`);
|
|
a(1, `}`);
|
|
}
|
|
a(0, ``);
|
|
}
|
|
|
|
a(0, `}`);
|
|
|
|
this.writeFormattedFile(
|
|
this.getFileName(def.name + "_client"),
|
|
lines.join("\n")
|
|
);
|
|
}
|
|
|
|
generateServiceServer(def: ServiceDefinition) {
|
|
let lines: string[] = [];
|
|
const a: lineAppender = (i, t) => {
|
|
if (!Array.isArray(t)) {
|
|
t = [t];
|
|
}
|
|
t.forEach((l) => lines.push(" ".repeat(i) + l.trim()));
|
|
};
|
|
|
|
this.writeFile(
|
|
"service_server.ts",
|
|
this.generateImport(
|
|
"{ RequestObject, ResponseObject, ErrorCodes, Logging }",
|
|
"./service_base"
|
|
) +
|
|
"\n\n" +
|
|
this.getTemplate("ts_service_server.ts")
|
|
);
|
|
|
|
this.generateImports(a, def);
|
|
|
|
a(0, `export type {`);
|
|
def.depends.forEach((dep) => {
|
|
a(1, `${dep},`);
|
|
});
|
|
a(0, `}`);
|
|
|
|
a(0, this.generateImport("{ Service }", "./service_server"));
|
|
|
|
a(
|
|
0,
|
|
this.generateImport(
|
|
"{ verify_number, verify_string, verify_boolean }",
|
|
"./service_base"
|
|
)
|
|
);
|
|
|
|
a(0, ``);
|
|
|
|
a(0, `export abstract class ${def.name}<T> extends Service<T> {`);
|
|
a(1, `public name = "${def.name}";`);
|
|
a(1, `constructor(){`);
|
|
a(2, `super();`);
|
|
for (const fnc of def.functions) {
|
|
a(2, `this.functions.add("${fnc.name}")`);
|
|
}
|
|
a(1, `}`);
|
|
a(0, ``);
|
|
|
|
for (const fnc of def.functions) {
|
|
const params = [
|
|
...fnc.inputs.map((e) => `${e.name}: ${toJSType(e.type)}`),
|
|
`ctx: T`,
|
|
].join(", ");
|
|
const retVal = fnc.return
|
|
? `Promise<${toJSType(fnc.return)}>`
|
|
: `void`;
|
|
a(1, `abstract ${fnc.name}(${params}): ${retVal};`);
|
|
|
|
// a(0, ``);
|
|
|
|
a(1, `_${fnc.name}(params: any[] | any, ctx: T): ${retVal} {`);
|
|
a(2, `let p: any[] = [];`);
|
|
a(2, `if(Array.isArray(params)){`);
|
|
a(3, `p = params;`);
|
|
a(2, `} else {`);
|
|
for (const param of fnc.inputs) {
|
|
a(3, `p.push(params["${param.name}"])`);
|
|
}
|
|
a(2, `}`);
|
|
a(2, ``);
|
|
|
|
for(let i = 0; i < fnc.inputs.length; i++) {
|
|
a(2, `if(p[${i}] !== null && p[${i}] !== undefined) {`);
|
|
a(2, `if(!verify_${fnc.inputs[i].type}(p[${i}])) throw new Error("Parameter verification failed!")`);
|
|
a(2, `}`);
|
|
}
|
|
|
|
a(2, ``);
|
|
a(2, `p.push(ctx);`);
|
|
a(2, `return this.${fnc.name}.call(this, ...p);`);
|
|
a(1, `}`);
|
|
a(0, ``);
|
|
}
|
|
|
|
a(0, `}`);
|
|
|
|
this.writeFormattedFile(
|
|
this.getFileName(def.name + "_server"),
|
|
lines.join("\n")
|
|
);
|
|
}
|
|
|
|
generateService(def: ServiceDefinition) {
|
|
this.writeFile("service_base.ts", this.getTemplate("ts_service_base.ts"));
|
|
this.generateServiceClient(def);
|
|
this.generateServiceServer(def);
|
|
}
|
|
|
|
finalize(steps: Step[]) {
|
|
let linesClient: string[] = [];
|
|
let linesServer: string[] = [];
|
|
|
|
const ac: lineAppender = (i, t) => {
|
|
if (!Array.isArray(t)) {
|
|
t = [t];
|
|
}
|
|
t.forEach((l) => linesClient.push(" ".repeat(i) + l.trim()));
|
|
};
|
|
|
|
const as: lineAppender = (i, t) => {
|
|
if (!Array.isArray(t)) {
|
|
t = [t];
|
|
}
|
|
t.forEach((l) => linesServer.push(" ".repeat(i) + l.trim()));
|
|
};
|
|
|
|
let lines: string[] = [];
|
|
const a: lineAppender = (i, t) => {
|
|
if (!Array.isArray(t)) {
|
|
t = [t];
|
|
}
|
|
t.forEach((l) => lines.push(" ".repeat(i) + l.trim()));
|
|
};
|
|
|
|
let hasService = false;
|
|
steps.forEach(([type, def]) => {
|
|
switch (type) {
|
|
case "type":
|
|
a(
|
|
0,
|
|
this.generateImport(
|
|
`${def.name}, { verify_${def.name} }`,
|
|
"./" + def.name
|
|
)
|
|
);
|
|
|
|
a(0, `export { verify_${def.name} }`);
|
|
a(0, `export type { ${def.name} }`);
|
|
a(0, ``);
|
|
break;
|
|
case "enum":
|
|
a(
|
|
0,
|
|
this.generateImport(
|
|
`${def.name}, { verify_${def.name} }`,
|
|
"./" + def.name
|
|
)
|
|
);
|
|
a(0, `export { ${def.name}, verify_${def.name} }`);
|
|
a(0, ``);
|
|
break;
|
|
|
|
case "service":
|
|
let ext = this.flavour == "esm" ? ".ts" : "";
|
|
if (!hasService) {
|
|
hasService = true;
|
|
ac(0, `export * from "./service_client${ext}"`);
|
|
ac(0, ``);
|
|
|
|
as(0, `export * from "./service_server${ext}"`);
|
|
as(0, ``);
|
|
|
|
a(0, `export * as Client from "./index_client${ext}"`);
|
|
a(0, `export * as Server from "./index_server${ext}"`);
|
|
a(0, `export { Logging } from "./service_base${ext}"`);
|
|
a(0, ``);
|
|
}
|
|
|
|
ac(
|
|
0,
|
|
`export { ${def.name} } from "./${def.name}_client${ext}"`
|
|
);
|
|
as(
|
|
0,
|
|
`export { ${def.name} } from "./${def.name}_server${ext}"`
|
|
);
|
|
ac(0, ``);
|
|
as(0, ``);
|
|
break;
|
|
}
|
|
});
|
|
|
|
this.writeFormattedFile(this.getFileName("index"), lines.join("\n"));
|
|
|
|
this.writeFormattedFile(
|
|
this.getFileName("index_client"),
|
|
linesClient.join("\n")
|
|
);
|
|
|
|
this.writeFormattedFile(
|
|
this.getFileName("index_server"),
|
|
linesServer.join("\n")
|
|
);
|
|
}
|
|
}
|
|
|
|
export class ESMTypescriptTarget extends TypescriptTarget {
|
|
name = "ts-esm";
|
|
flavour: "esm" = "esm";
|
|
}
|
|
|
|
export class NodeJSTypescriptTarget extends TypescriptTarget {
|
|
name = "ts-node";
|
|
flavour: "node" = "node";
|
|
}
|