Adding Decorators for comments

This commit is contained in:
K35 2022-01-01 18:47:34 +00:00
parent 579055d8fb
commit 4e389dc472
7 changed files with 145 additions and 15 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ node_modules/
.yarn/cache
.yarn/install-state.gz
examples/out
examples/definition.json

View File

@ -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);
}

View File

@ -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"
},

View File

@ -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;
});

View File

@ -44,11 +44,15 @@ export interface IServiceFunctionInput {
type: string;
}
export type Decorators = Map<string, string[][]>;
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("}");

View File

@ -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<string>;
};
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;
}
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)
if(!parsed)
throw new Error("Error compiling: Parse output is undefined!");
const parsed = processFile(ctx, options.input, true);
if(!parsed)
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) {

View File

@ -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"),