diff --git a/.gitignore b/.gitignore index fe4cc9f..a430c18 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ .yarn/cache .yarn/install-state.gz examples/out +examples/definition.json diff --git a/examples/example.jrpc b/examples/example.jrpc index 7989bf0..a5177d1 100644 --- a/examples/example.jrpc +++ b/examples/example.jrpc @@ -26,9 +26,20 @@ type AddValueResponse { } service TestService { + @Description("Add two numbers") + @Param("request", "Parameter containing the two numbers") AddValuesSingleParam(request: AddValueRequest): AddValueResponse; + + @Description("Add two numbers") + @Param("value1", "The first value") + @Param("value2", "The second value") AddValuesMultipleParams(value1: number, value2: number): number; + + @Description("Does literaly nothing") + @Param("param1", "Some number") ReturningVoid(param1: number): void; + @Description("Just sends an Event with a String") + @Param("param1", "Parameter with some string for event") notification OnEvent(param1: string); } diff --git a/package.json b/package.json index 10de980..1bb0424 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "@hibas123/jrpcgen", - "version": "1.0.3", + "version": "1.0.4", "main": "lib/index.js", "license": "MIT", "packageManager": "yarn@3.1.1", "scripts": { "start": "ts-node src/index.ts", - "test": "npm run start -- compile examples/example.jrpc -o=ts-node:examples/out && ts-node examples/test.ts", + "test": "npm run start -- compile examples/example.jrpc --definition=examples/definition.json -o=ts-node:examples/out && ts-node examples/test.ts", "build": "esbuild src/index.ts --bundle --platform=node --target=node14 --outfile=lib/jrpc.js", "prepublishOnly": "npm run build" }, diff --git a/src/ir.ts b/src/ir.ts index 173004f..e31c159 100644 --- a/src/ir.ts +++ b/src/ir.ts @@ -33,10 +33,20 @@ export interface EnumDefinition { values: EnumValueDefinition[]; } +export type ServiceFunctionDecorators = { + description: string; + parameters: { + name:string; + description: string; + }[]; + returns: string; +} + export interface ServiceFunctionParamsDefinition { name: string; inputs: { type: string; name: string }[]; return: string | undefined; + decorators: ServiceFunctionDecorators } export type ServiceFunctionDefinition = ServiceFunctionParamsDefinition; @@ -208,10 +218,53 @@ export default function get_ir(parsed: Parsed): IR { } } + let decorators = {} as ServiceFunctionDecorators; + + fnc.decorators.forEach((values, key)=>{ + for(const val of values) { + switch(key) { + case "Description": + if(decorators.description) + throw new IRError(fnc, `Decorator 'Description' can only be used once!`); + if(val.length != 1) + throw new IRError(fnc, `Decorator 'Description' requires exactly one parameter!`); + decorators.description = val[0]; + break; + case "Returns": + if(decorators.returns) + throw new IRError(fnc, `Decorator 'Returns' can only be used once!`); + if(val.length != 1) + throw new IRError(fnc, `Decorator 'Returns' requires exactly one parameter!`); + decorators.returns = val[0]; + break; + case "Param": + if(!decorators.parameters) + decorators.parameters = []; + if(val.length != 2) + throw new IRError(fnc, `Decorator 'Param' requires exactly two parameters!`); + const [name, description] = val; + if(!fnc.inputs.find(e=>e.name == name)) + throw new IRError(fnc, `Decorator 'Param' requires the first param to equal the name of a function parameter!`); + if(decorators.parameters.find(e=>e.name == name)) + throw new IRError(fnc, `Decorator 'Param' has already been set for the parameter ${name}!`); + + decorators.parameters.push({ + name, + description, + }) + break; + default: + throw new IRError(fnc, `Decorator ${key} is not a valid decorator!`); + } + } + }) + + return { name: fnc.name, inputs: fnc.inputs, return: fnc.return_type, + decorators } as ServiceFunctionDefinition; }); diff --git a/src/parser.ts b/src/parser.ts index a5d8419..4b79773 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -44,11 +44,15 @@ export interface IServiceFunctionInput { type: string; } + +export type Decorators = Map; + export interface ServiceFunctionStatement extends DefinitionNode { type: "service_function"; inputs: IServiceFunctionInput[]; name: string; return_type: string | undefined; // Makes it a notification + decorators: Decorators; } export interface ServiceStatement extends DefinitionNode { @@ -116,6 +120,8 @@ export default function parse(tokens: Token[], file: string): Parsed { return val; }; + + const checkTypes = (...types: string[]) => { if (types.indexOf(currentToken.type) < 0) { throw new ParserError( @@ -277,8 +283,44 @@ export default function parse(tokens: Token[], file: string): Parsed { }; }; + const parseFunctionDecorator = (decorators: Decorators = new Map) => { + const idx = eatToken("@"); + const [decorator] = eatText(); + eatToken("("); + let args: string[] = []; + let first = true; + while(currentToken.value !== ")") { + if(first) { + first= false; + } else { + eatToken(","); + } + checkTypes("string"); + // if(currentToken.type == "number") { + // let arg = Number(currentToken.value); + // if (Number.isNaN(arg)) { + // throw new ParserError( + // `Value cannot be parsed as number! ${currentToken.value}`, + // currentToken + // ); + // } + // } + args.push(currentToken.value.slice(1, -1)); + eatToken(); + + } + eatToken(")"); + + let dec = decorators.get(decorator) || []; + dec.push(args); + decorators.set(decorator, dec); + + return decorators; + } + const parseServiceFunction = ( - notification?: boolean + decorators: Decorators, + notification?: boolean, ): ServiceFunctionStatement => { const [name, idx] = eatText(); @@ -318,6 +360,7 @@ export default function parse(tokens: Token[], file: string): Parsed { }, inputs, return_type, + decorators }; }; @@ -327,13 +370,19 @@ export default function parse(tokens: Token[], file: string): Parsed { eatToken("{"); let functions: ServiceFunctionStatement[] = []; - while (currentToken.type === "text") { + while (currentToken.type !== "curly_close") { + let decorators:Decorators = new Map; + while(currentToken.type == "at") { + parseFunctionDecorator(decorators); + } + + let notification = false; if (currentToken.value == "notification") { eatText(); notification = true; } - functions.push(parseServiceFunction(notification)); + functions.push(parseServiceFunction(decorators, notification)); } eatToken("}"); diff --git a/src/process.ts b/src/process.ts index 972f6cb..ba2f25d 100644 --- a/src/process.ts +++ b/src/process.ts @@ -4,10 +4,12 @@ import Color from "chalk"; import * as Path from "path"; import tokenize, { TokenizerError } from "./tokenizer"; import parse, { Parsed, ParserError } from "./parser"; -import get_ir, { IR } from "./ir"; +import get_ir, { IR, IRError } from "./ir"; import compile, { CompileTarget } from "./compile"; import { ESMTypescriptTarget, NodeJSTypescriptTarget } from "./targets/typescript"; +class CatchedError extends Error {} + const log = dbg("app"); @@ -75,7 +77,7 @@ type ProcessContext = { processedFiles: Set; }; -function processFile(ctx: ProcessContext, file: string): Parsed | null { +function processFile(ctx: ProcessContext, file: string, root = false): Parsed | null { file = Path.resolve(file); if (ctx.processedFiles.has(file)) { log("Skipping file %s since it has already be processed", file); @@ -109,13 +111,17 @@ function processFile(ctx: ProcessContext, file: string): Parsed | null { } catch (err) { if (err instanceof TokenizerError) { printError(err, file, err.index); + if(!root) + throw new CatchedError(); } else if (err instanceof ParserError) { printError(err, file, err.token.startIdx); + if(!root) + throw new CatchedError(); + } else if(root && err instanceof CatchedError) { + return null; } else { - throw err; + throw err; } - - return null; } } @@ -129,19 +135,27 @@ export default function startCompile(options: CompileOptions) { if(options.input.endsWith(".json")) { ir = JSON.parse(FS.readFileSync(options.input, "utf-8")); } else { - const parsed = processFile(ctx, options.input); - // console.log(([...parsed].pop() as any).functions) + const parsed = processFile(ctx, options.input, true); + if(!parsed) - throw new Error("Error compiling: Parse output is undefined!"); - + process.exit(1); // Errors should have already been emitted + try { ir = get_ir(parsed); + } catch(err) { + if(err instanceof IRError) { + printError(err, err.statement.location.file, err.statement.location.idx); + process.exit(1); + } else { + throw err; + } + } } if(!ir) throw new Error("Error compiling: Cannot get IR"); if(options.emitDefinitions) { - FS.writeFileSync(options.emitDefinitions, JSON.stringify(ir)); + FS.writeFileSync(options.emitDefinitions, JSON.stringify(ir, undefined, 3)); } if(options.targets.length <= 0) { diff --git a/src/tokenizer.ts b/src/tokenizer.ts index 3c5bee8..952afd2 100644 --- a/src/tokenizer.ts +++ b/src/tokenizer.ts @@ -3,6 +3,7 @@ export type TokenTypes = | "comment" | "string" | "keyword" + | "at" | "colon" | "semicolon" | "comma" @@ -57,6 +58,7 @@ const matcher = [ regexMatcher(/^".*?"/, "string"), // regexMatcher(/(?<=^")(.*?)(?=")/, "string"), regexMatcher(/^(type|enum|import|service)\b/, "keyword"), + regexMatcher(/^\@/, "at"), regexMatcher(/^\:/, "colon"), regexMatcher(/^\;/, "semicolon"), regexMatcher(/^\,/, "comma"),