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} extends Service {`); 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"; }