JsonRPC/src/targets/typescript.ts

427 lines
12 KiB
TypeScript

import {
TypeDefinition,
ServiceDefinition,
EnumDefinition,
TypeFieldDefinition,
Step,
} from "../ir";
import { CompileTarget, } from "../compile";
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);
//TODO: Add Prettier back
// const formatted = 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, `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 ap: lineAppender = (i, l) => a(i + 2, l);
const verifyType = ( )=>{};
a(2, "// TODO: Implement")
//TODO: Build verification
// if (field.array) {
// a(2, `if(!Array.isArray(data["${field.name}"])) return false`);
// a(2, `if(!(data["${field.name}"].some(e=>))) return false`)
// serializeArray(field, `data["${field.name}"]`, ap);
// } else if (field.map) {
// serializeMap(field, `data["${field.name}"]`, ap);
// } else {
// serializeType(field, `data["${field.name}"]`, true, ap);
// }
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) {
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, `}`);
this.writeFile(
"service_client.ts",
this.generateImport(
"{ RequestObject, ResponseObject, ErrorCodes, Logging }",
"./service_base"
) +
"\n\n" +
this.getTemplate("ts_service_client.ts")
);
a(
0,
this.generateImport(
"{ Service, ServiceProvider, getRandomID }",
"./service_client"
)
);
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 1 : Verify response!
//TODO: Prio 2 : Add optional parameters to this and the declaration file
//TODO: Prio 3 : Maybe verify params? But the server will to this regardless so... Maybe not?`
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, `});`);
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, `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)){`);
//TODO: Verify 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, `p.push(ctx);`) //TODO: Either this or [...p, ctx] but idk
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,``)
//TODO: Export service globals
}
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";
}