Adding parameter and response validation.

Adding prettier formatting support.
Add warning when no targets are selected.
This commit is contained in:
K35 2022-01-01 00:39:15 +00:00
parent cc6035eef0
commit 7da1cf0841
7 changed files with 248 additions and 132 deletions

View File

@ -1,54 +1,71 @@
import { AddValueRequest, AddValueResponse, Logging } from "./out/index" import { AddValueRequest, AddValueResponse, Logging } from "./out/index";
// Logging.verbose = false; // Logging.verbose = false;
import * as Client from "./out/index_client" import * as Client from "./out/index_client";
import * as Server from "./out/index_server" import * as Server from "./out/index_server";
const client = new Client.ServiceProvider(msg=>{ const client = new Client.ServiceProvider((msg) => {
session.onMessage(msg); session.onMessage(msg);
}) });
const server = new Server.ServiceProvider() const server = new Server.ServiceProvider();
const session = server.getSession((msg) => { const session = server.getSession((msg) => {
client.onPacket(msg); client.onPacket(msg);
}) });
class TestService extends Server.TestService<undefined> { class TestService extends Server.TestService<undefined> {
async AddValuesSingleParam(request: AddValueRequest, ctx: undefined): Promise<AddValueResponse> { async AddValuesSingleParam(
request: AddValueRequest,
ctx: undefined
): Promise<AddValueResponse> {
return { return {
value: request.value1 + request.value2 value: request.value1 + request.value2,
};
} }
} async AddValuesMultipleParams(
async AddValuesMultipleParams(value1: number, value2: number, ctx: undefined): Promise<number> { value1: number,
value2: number,
ctx: undefined
): Promise<number> {
return value1 + value2; return value1 + value2;
} }
OnEvent(param1: string, ctx: undefined): void { OnEvent(param1: string, ctx: undefined): void {
console.log("Received notification", param1); console.log("Received notification", param1);
} }
} }
server.addService(new TestService()) server.addService(new TestService());
const test = new Client.TestService(client); const test = new Client.TestService(client);
async function run() { async function run() {
console.log("Testing AddValuesSingleParam") console.log("Testing AddValuesSingleParam");
console.log(await test.AddValuesSingleParam({ console.log(
await test.AddValuesSingleParam({
value1: 1, value1: 1,
value2: 2 value2: 2,
})); })
);
console.log("Testing AddValuesMultipleParams") console.log("Testing AddValuesMultipleParams");
console.log(await test.AddValuesMultipleParams(1,2)); console.log(await test.AddValuesMultipleParams(1, 2));
console.log("Testing Notification") console.log("Testing Notification");
test.OnEvent("Hi, this is an event"); test.OnEvent("Hi, this is an event");
console.log("Let verification fail!");
await test
//@ts-ignore
.AddValuesMultipleParams("asd", 2)
.then(() => {
console.log("!!!!This should have failed!!!!");
})
.catch((err) => {
console.log("Found expected error!", err.message);
});
} }
run(); run();

View File

@ -11,6 +11,7 @@
"devDependencies": { "devDependencies": {
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",
"@types/node": "^17.0.5", "@types/node": "^17.0.5",
"@types/prettier": "^2.4.2",
"@types/yargs": "^17.0.8", "@types/yargs": "^17.0.8",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"typescript": "^4.5.4" "typescript": "^4.5.4"

View File

@ -143,12 +143,16 @@ export default function startCompile(options: CompileOptions) {
FS.writeFileSync(options.emitDefinitions, JSON.stringify(ir)); FS.writeFileSync(options.emitDefinitions, JSON.stringify(ir));
} }
if(options.targets.length <= 0) {
console.log(Color.yellow("WARNING:"), "No targets selected!");
}
options.targets.forEach(target => { options.targets.forEach(target => {
const tg = Targets.get(target.type) as any const tg = Targets.get(target.type) as any
if(!tg) { if(!tg) {
console.log(Color.red("ERROR:"), "Target not supported!"); console.log(Color.red("ERROR:"), "Target not supported!");
return; return;
} }
compile(ir, new tg(target.output)); //TODO: implement compile(ir, new tg(target.output));
}) })
} }

View File

@ -6,7 +6,8 @@ import {
Step, Step,
} from "../ir"; } from "../ir";
import { CompileTarget, } from "../compile"; import { CompileTarget } from "../compile";
import * as prettier from "prettier";
type lineAppender = (ind: number, line: string | string[]) => void; type lineAppender = (ind: number, line: string | string[]) => void;
@ -40,10 +41,7 @@ export class TypescriptTarget extends CompileTarget {
a( a(
0, 0,
def.depends.map((dep) => def.depends.map((dep) =>
this.generateImport( this.generateImport(`${dep}, { verify_${dep} }`, "./" + dep)
`${dep}, { verify_${dep} }`,
"./" + dep
)
) )
); );
} }
@ -53,14 +51,13 @@ export class TypescriptTarget extends CompileTarget {
} }
private writeFormattedFile(file: string, code: string) { private writeFormattedFile(file: string, code: string) {
this.writeFile(file, code); // this.writeFile(file, code);
//TODO: Add Prettier back const formatted = prettier.format(code, {
// const formatted = format(code, { parser: "typescript",
// parser: "typescript", tabWidth: 3,
// tabWidth: 3, });
// });
// this.writeFile(file, formatted); this.writeFile(file, formatted);
} }
generateType(def: TypeDefinition) { generateType(def: TypeDefinition) {
@ -91,23 +88,24 @@ export class TypescriptTarget extends CompileTarget {
a(0, ``); a(0, ``);
a(1, `constructor(init?:Partial<${def.name}>){`); a(1, `constructor(init?:Partial<${def.name}>){`);
a(2, `if(init){`) a(2, `if(init){`);
def.fields.forEach(field=>{ def.fields.forEach((field) => {
a(3, `if(init["${field.name}"])`) a(3, `if(init["${field.name}"])`);
a(4, `this.${field.name} = init["${field.name}"];`) a(4, `this.${field.name} = init["${field.name}"];`);
}) });
a(2, `}`); a(2, `}`);
a(1, `}`); a(1, `}`);
a(0, ``); a(0, ``);
a(0, ``); a(0, ``);
a(1, `static verify(data: ${def.name}){`); a(1, `static verify(data: ${def.name}){`);
a(2, `return verify_${def.name}(data);`); a(2, `return verify_${def.name}(data);`);
a(1, `}`) a(1, `}`);
a(0, "}"); a(0, `}`);
a(0, ``);
a(0, `export function verify_${def.name}(data: ${def.name}): boolean {`); a(0, `export function verify_${def.name}(data: ${def.name}): boolean {`);
{ {
@ -117,19 +115,41 @@ export class TypescriptTarget extends CompileTarget {
`if(data["${field.name}"] !== null && data["${field.name}"] !== undefined ) {` `if(data["${field.name}"] !== null && data["${field.name}"] !== undefined ) {`
); );
const ap: lineAppender = (i, l) => a(i + 2, l); const verifyType = (varName: string) => {
const verifyType = ( )=>{}; switch (field.type) {
a(2, "// TODO: Implement") case "string":
//TODO: Build verification a(2, `if(typeof ${varName} !== "string") return false;`);
// if (field.array) { break;
// a(2, `if(!Array.isArray(data["${field.name}"])) return false`); case "number":
// a(2, `if(!(data["${field.name}"].some(e=>))) return false`) a(2, `if(typeof ${varName} !== "number") return false;`);
// serializeArray(field, `data["${field.name}"]`, ap); break;
// } else if (field.map) { case "boolean":
// serializeMap(field, `data["${field.name}"]`, ap); a(2, `if(typeof ${varName} !== "boolean") return false;`);
// } else { break;
// serializeType(field, `data["${field.name}"]`, true, ap); 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(1, "}");
a(0, ``); a(0, ``);
}); });
@ -157,18 +177,25 @@ export class TypescriptTarget extends CompileTarget {
a(0, `export default ${def.name}`); a(0, `export default ${def.name}`);
a( a(0, `export function verify_${def.name} (data: ${def.name}): boolean {`);
0,
`export function verify_${def.name} (data: ${def.name}): boolean {`
);
a(1, `return ${def.name}[data] != undefined`); a(1, `return ${def.name}[data] != undefined`);
a(0, "}"); a(0, "}");
this.writeFormattedFile(this.getFileName(def.name), lines.join("\n")); this.writeFormattedFile(this.getFileName(def.name), lines.join("\n"));
} }
generateServiceClient(def: ServiceDefinition) { 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[] = []; let lines: string[] = [];
const a: lineAppender = (i, t) => { const a: lineAppender = (i, t) => {
if (!Array.isArray(t)) { if (!Array.isArray(t)) {
@ -185,14 +212,12 @@ export class TypescriptTarget extends CompileTarget {
}); });
a(0, `}`); a(0, `}`);
this.writeFile( a(
"service_client.ts", 0,
this.generateImport( this.generateImport(
"{ RequestObject, ResponseObject, ErrorCodes, Logging }", "{ verify_number, verify_string, verify_boolean }",
"./service_base" "./service_base"
) + )
"\n\n" +
this.getTemplate("ts_service_client.ts")
); );
a( a(
@ -203,17 +228,19 @@ export class TypescriptTarget extends CompileTarget {
) )
); );
a(0, ``);
a(0, `export class ${def.name} extends Service {`); a(0, `export class ${def.name} extends Service {`);
a(1, `constructor(provider: ServiceProvider){`); a(1, `constructor(provider: ServiceProvider){`);
a(2, `super(provider, "${def.name}");`); a(2, `super(provider, "${def.name}");`);
a(1, `}`); a(1, `}`);
for(const fnc of def.functions) { for (const fnc of def.functions) {
const params = fnc.inputs.map(e=>`${e.name}: ${toJSType(e.type)}`).join(","); const params = fnc.inputs
//TODO: Prio 1 : Verify response! .map((e) => `${e.name}: ${toJSType(e.type)}`)
.join(",");
//TODO: Prio 2 : Add optional parameters to this and the declaration file //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) {
if(!fnc.return) {
a(1, `${fnc.name}(${params}): void {`); a(1, `${fnc.name}(${params}): void {`);
a(2, `this._provider.sendMessage({`); a(2, `this._provider.sendMessage({`);
a(3, `jsonrpc: "2.0",`); a(3, `jsonrpc: "2.0",`);
@ -224,7 +251,7 @@ export class TypescriptTarget extends CompileTarget {
} else { } else {
const retType = fnc.return ? toJSType(fnc.return) : "void"; const retType = fnc.return ? toJSType(fnc.return) : "void";
a(1, `${fnc.name}(${params}): Promise<${retType}> {`); a(1, `${fnc.name}(${params}): Promise<${retType}> {`);
a(2, `return new Promise<${retType}>((ok, err) => {`) a(2, `return new Promise<${retType}>((ok, err) => {`);
a(3, `this._provider.sendMessage({`); a(3, `this._provider.sendMessage({`);
a(4, `jsonrpc: "2.0",`); a(4, `jsonrpc: "2.0",`);
a(4, `id: getRandomID(16),`); a(4, `id: getRandomID(16),`);
@ -233,6 +260,12 @@ export class TypescriptTarget extends CompileTarget {
a(3, `}, {`); a(3, `}, {`);
a(4, `ok, err`); a(4, `ok, err`);
a(3, `});`); 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(2, `});`);
a(1, `}`); a(1, `}`);
} }
@ -274,49 +307,65 @@ export class TypescriptTarget extends CompileTarget {
}); });
a(0, `}`); a(0, `}`);
a(0, this.generateImport("{ Service }", "./service_server"));
a( a(
0, 0,
this.generateImport( this.generateImport(
"{ Service }", "{ verify_number, verify_string, verify_boolean }",
"./service_server" "./service_base"
) )
); );
a(0, ``);
a(0, `export abstract class ${def.name}<T> extends Service<T> {`); a(0, `export abstract class ${def.name}<T> extends Service<T> {`);
a(1, `public name = "${def.name}";`); a(1, `public name = "${def.name}";`);
a(1, `constructor(){`); a(1, `constructor(){`);
a(2, `super();`); a(2, `super();`);
for(const fnc of def.functions) { for (const fnc of def.functions) {
a(2, `this.functions.add("${fnc.name}")`); a(2, `this.functions.add("${fnc.name}")`);
} }
a(1, `}`) a(1, `}`);
a(0, ``); a(0, ``);
for(const fnc of def.functions) { for (const fnc of def.functions) {
const params = [...fnc.inputs.map(e=>`${e.name}: ${toJSType(e.type)}`), `ctx: T`].join(", "); const params = [
const retVal = fnc.return ? `Promise<${toJSType(fnc.return)}>` : `void`; ...fnc.inputs.map((e) => `${e.name}: ${toJSType(e.type)}`),
a(1, `abstract ${fnc.name}(${params}): ${retVal};`) `ctx: T`,
].join(", ");
const retVal = fnc.return
? `Promise<${toJSType(fnc.return)}>`
: `void`;
a(1, `abstract ${fnc.name}(${params}): ${retVal};`);
// a(0, ``); // a(0, ``);
a(1, `_${fnc.name}(params: any[] | any, ctx: T): ${retVal} {`); a(1, `_${fnc.name}(params: any[] | any, ctx: T): ${retVal} {`);
a(2, `let p: any[] = [];`); a(2, `let p: any[] = [];`);
a(2, `if(Array.isArray(params)){`); a(2, `if(Array.isArray(params)){`);
//TODO: Verify params!
a(3, `p = params;`); a(3, `p = params;`);
a(2, `} else {`); a(2, `} else {`);
for(const param of fnc.inputs) { for (const param of fnc.inputs) {
a(3, `p.push(params["${param.name}"])`); a(3, `p.push(params["${param.name}"])`);
} }
a(2, `}`); a(2, `}`);
a(2, `p.push(ctx);`) //TODO: Either this or [...p, ctx] but idk 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(2, `return this.${fnc.name}.call(this, ...p);`);
a(1, `}`) a(1, `}`);
a(0, ``); a(0, ``);
} }
a(0, `}`) a(0, `}`);
this.writeFormattedFile( this.writeFormattedFile(
this.getFileName(def.name + "_server"), this.getFileName(def.name + "_server"),
@ -348,7 +397,7 @@ export class TypescriptTarget extends CompileTarget {
t.forEach((l) => linesServer.push(" ".repeat(i) + l.trim())); t.forEach((l) => linesServer.push(" ".repeat(i) + l.trim()));
}; };
let lines:string[] = []; let lines: string[] = [];
const a: lineAppender = (i, t) => { const a: lineAppender = (i, t) => {
if (!Array.isArray(t)) { if (!Array.isArray(t)) {
t = [t]; t = [t];
@ -356,52 +405,65 @@ export class TypescriptTarget extends CompileTarget {
t.forEach((l) => lines.push(" ".repeat(i) + l.trim())); t.forEach((l) => lines.push(" ".repeat(i) + l.trim()));
}; };
let hasService = false; let hasService = false;
steps.forEach(([type, def]) => { steps.forEach(([type, def]) => {
switch(type) { switch (type) {
case "type": case "type":
a(0, this.generateImport(`${def.name}, { verify_${def.name} }`, "./" + def.name)) a(
0,
this.generateImport(
`${def.name}, { verify_${def.name} }`,
"./" + def.name
)
);
a(0, `export { verify_${def.name} }`) a(0, `export { verify_${def.name} }`);
a(0, `export type { ${def.name} }`); a(0, `export type { ${def.name} }`);
a(0,``); a(0, ``);
break; break;
case "enum": case "enum":
a(0, this.generateImport(`${def.name}, { verify_${def.name} }`, "./" + def.name)) a(
a(0, `export { ${def.name}, verify_${def.name} }`) 0,
a(0,``); this.generateImport(
`${def.name}, { verify_${def.name} }`,
"./" + def.name
)
);
a(0, `export { ${def.name}, verify_${def.name} }`);
a(0, ``);
break; break;
case "service": case "service":
let ext = this.flavour == "esm" ? ".ts" : ""; let ext = this.flavour == "esm" ? ".ts" : "";
if(!hasService) { if (!hasService) {
hasService = true; hasService = true;
ac(0, `export * from "./service_client${ext}"`); ac(0, `export * from "./service_client${ext}"`);
ac(0,``); ac(0, ``);
as(0, `export * from "./service_server${ext}"`); as(0, `export * from "./service_server${ext}"`);
as(0,``); as(0, ``);
a(0, `export * as Client from "./index_client${ext}"`); a(0, `export * as Client from "./index_client${ext}"`);
a(0, `export * as Server from "./index_server${ext}"`); a(0, `export * as Server from "./index_server${ext}"`);
a(0, `export { Logging } from "./service_base${ext}"`); a(0, `export { Logging } from "./service_base${ext}"`);
a(0,``) a(0, ``);
//TODO: Export service globals
} }
ac(0, `export { ${def.name} } from "./${def.name}_client${ext}"`); ac(
as(0, `export { ${def.name} } from "./${def.name}_server${ext}"`); 0,
ac(0,``); `export { ${def.name} } from "./${def.name}_client${ext}"`
as(0,``); );
as(
0,
`export { ${def.name} } from "./${def.name}_server${ext}"`
);
ac(0, ``);
as(0, ``);
break; break;
} }
}) });
this.writeFormattedFile( this.writeFormattedFile(this.getFileName("index"), lines.join("\n"));
this.getFileName("index"),
lines.join("\n")
);
this.writeFormattedFile( this.writeFormattedFile(
this.getFileName("index_client"), this.getFileName("index_client"),

View File

@ -1,18 +1,18 @@
export const Logging = { export const Logging = {
verbose: false, verbose: false,
log(...args: any[]) { log(...args: any[]) {
if(Logging.verbose) { if (Logging.verbose) {
console.log(...args) console.log(...args);
} }
} },
} };
export enum ErrorCodes { export enum ErrorCodes {
ParseError=-32700, ParseError = -32700,
InvalidRequest=-32700, InvalidRequest = -32700,
MethodNotFound=-32700, MethodNotFound = -32700,
InvalidParams=-32700, InvalidParams = -32700,
InternalError=-32700, InternalError = -32700,
} }
export interface RequestObject { export interface RequestObject {
@ -25,6 +25,24 @@ export interface RequestObject {
export interface ResponseObject { export interface ResponseObject {
jsonrpc: "2.0"; jsonrpc: "2.0";
result?: any; result?: any;
error?: { code: ErrorCodes, message:string, data?: any} error?: { code: ErrorCodes; message: string; data?: any };
id: string; id: string;
} }
export function verify_number(data: any) {
if (typeof data !== "number") return false;
return true;
}
export function verify_string(data: any) {
if (typeof data !== "string") return false;
return true;
}
export function verify_boolean(data: any) {
if (typeof data !== "boolean") return false;
return true;
}

View File

@ -34,7 +34,13 @@ export class ServiceProvider {
} else { } else {
Logging.log("CLIENT: Determined type is Notification"); Logging.log("CLIENT: Determined type is Notification");
//Notification. Send to Notification handler //Notification. Send to Notification handler
//TODO: Implement const [srvName, fncName] = msg.method.split(".");
let service = this.services.get(srvName)
if(!service) {
Logging.log("CLIENT: Did not find Service wanted by Notification!", srvName);
} else {
//TODO: Implement Event thingy (or so :))
}
} }
} else { } else {
Logging.log("CLIENT: Determined type is Response"); Logging.log("CLIENT: Determined type is Response");

View File

@ -21,12 +21,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@hibas123/SimpleRPC@workspace:.": "@hibas123/jrpcgen@workspace:.":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@hibas123/SimpleRPC@workspace:." resolution: "@hibas123/jrpcgen@workspace:."
dependencies: dependencies:
"@types/debug": ^4.1.7 "@types/debug": ^4.1.7
"@types/node": ^17.0.5 "@types/node": ^17.0.5
"@types/prettier": ^2.4.2
"@types/yargs": ^17.0.8 "@types/yargs": ^17.0.8
chalk: 4 chalk: 4
debug: ^4.3.3 debug: ^4.3.3
@ -88,6 +89,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/prettier@npm:^2.4.2":
version: 2.4.2
resolution: "@types/prettier@npm:2.4.2"
checksum: 76e230b2d11028af11fe12e09b2d5b10b03738e9abf819ae6ebb0f78cac13d39f860755ce05ac3855b608222518d956628f5d00322dc206cc6d1f2d8d1519f1e
languageName: node
linkType: hard
"@types/yargs-parser@npm:*": "@types/yargs-parser@npm:*":
version: 20.2.1 version: 20.2.1
resolution: "@types/yargs-parser@npm:20.2.1" resolution: "@types/yargs-parser@npm:20.2.1"