Adding C# Support. Badly tested currently, but kindof working
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import * as FS from "fs";
|
||||
import * as FSE from "fs-extra"
|
||||
import * as Path from "path";
|
||||
import {
|
||||
EnumDefinition,
|
||||
@ -8,9 +9,9 @@ import {
|
||||
TypeDefinition,
|
||||
} from "./ir";
|
||||
|
||||
export abstract class CompileTarget {
|
||||
export abstract class CompileTarget<T = any> {
|
||||
abstract name: string;
|
||||
constructor(private outputFolder: string) {
|
||||
constructor(private outputFolder: string, protected options: T) {
|
||||
if (!FS.existsSync(outputFolder)) {
|
||||
FS.mkdirSync(outputFolder, {
|
||||
recursive: true,
|
||||
@ -57,6 +58,14 @@ export abstract class CompileTarget {
|
||||
|
||||
return res.join("\n");
|
||||
}
|
||||
|
||||
protected loadTemplateFolder(name:string) {
|
||||
let root = Path.join(__dirname, "../templates/", name);
|
||||
|
||||
FSE.copySync(root, this.outputFolder, {
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function compile(ir: IR, target: CompileTarget) {
|
||||
@ -64,12 +73,12 @@ export default function compile(ir: IR, target: CompileTarget) {
|
||||
|
||||
// setState("Building for target: " + target.name);
|
||||
target.start();
|
||||
ir.forEach((step) => {
|
||||
ir.steps.forEach((step) => {
|
||||
const [type, def] = step;
|
||||
if (type == "type") target.generateType(def as TypeDefinition);
|
||||
else if (type == "enum") target.generateEnum(def as EnumDefinition);
|
||||
else if (type == "service")
|
||||
target.generateService(def as ServiceDefinition);
|
||||
});
|
||||
if (target.finalize) target.finalize(ir);
|
||||
if (target.finalize) target.finalize(ir.steps);
|
||||
}
|
||||
|
13
src/ir.ts
13
src/ir.ts
@ -61,7 +61,10 @@ export type Step = [
|
||||
TypeDefinition | EnumDefinition | ServiceDefinition
|
||||
];
|
||||
|
||||
export type IR = Step[];
|
||||
export type IR = {
|
||||
options: { [key:string]: string},
|
||||
steps: Step[]
|
||||
};
|
||||
|
||||
export default function get_ir(parsed: Parsed): IR {
|
||||
log("Generatie IR from parse output");
|
||||
@ -72,6 +75,7 @@ export default function get_ir(parsed: Parsed): IR {
|
||||
// Verifiy and generating steps
|
||||
|
||||
let steps: Step[] = [];
|
||||
let options = {} as any;
|
||||
|
||||
parsed.forEach((statement) => {
|
||||
log("Working on statement of type %s", statement.type);
|
||||
@ -276,10 +280,15 @@ export default function get_ir(parsed: Parsed): IR {
|
||||
functions,
|
||||
} as ServiceDefinition,
|
||||
]);
|
||||
} else if(statement.type == "define") {
|
||||
options[statement.key] = statement.value;
|
||||
} else {
|
||||
throw new IRError(statement, "Invalid statement!");
|
||||
}
|
||||
});
|
||||
|
||||
return steps;
|
||||
return {
|
||||
options,
|
||||
steps
|
||||
};
|
||||
}
|
||||
|
@ -67,11 +67,19 @@ export interface ServiceStatement extends DefinitionNode {
|
||||
functions: ServiceFunctionStatement[];
|
||||
}
|
||||
|
||||
export interface DefineStatement extends DefinitionNode {
|
||||
type: "define";
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type RootStatementNode =
|
||||
| ImportStatement
|
||||
| TypeStatement
|
||||
| ServiceStatement
|
||||
| EnumStatement;
|
||||
| EnumStatement
|
||||
| DefineStatement;
|
||||
|
||||
export type StatementNode =
|
||||
| RootStatementNode
|
||||
| TypeFieldStatement
|
||||
@ -414,6 +422,22 @@ export default function parse(tokens: Token[], file: string): Parsed {
|
||||
};
|
||||
};
|
||||
|
||||
const parseDefine = (): DefineStatement => {
|
||||
const idx = eatToken("define");
|
||||
|
||||
let [key] = eatText()
|
||||
let [value] = eatText()
|
||||
|
||||
eatToken(";");
|
||||
|
||||
return {
|
||||
type :"define",
|
||||
location: {file, idx},
|
||||
key,
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
const parseStatement = () => {
|
||||
if (currentToken.type === "keyword") {
|
||||
switch (currentToken.value) {
|
||||
@ -427,6 +451,8 @@ export default function parse(tokens: Token[], file: string): Parsed {
|
||||
return parseEnumStatement();
|
||||
case "service":
|
||||
return parseServiceStatement();
|
||||
case "define":
|
||||
return parseDefine();
|
||||
default:
|
||||
throw new ParserError(
|
||||
`Unknown keyword ${currentToken.value}`,
|
||||
|
@ -6,18 +6,21 @@ import tokenize, { TokenizerError } from "./tokenizer";
|
||||
import parse, { Parsed, ParserError } from "./parser";
|
||||
import get_ir, { IR, IRError } from "./ir";
|
||||
import compile, { CompileTarget } from "./compile";
|
||||
import { ESMTypescriptTarget, NodeJSTypescriptTarget } from "./targets/typescript";
|
||||
import {
|
||||
ESMTypescriptTarget,
|
||||
NodeJSTypescriptTarget,
|
||||
} from "./targets/typescript";
|
||||
import { CSharpTarget } from "./targets/csharp";
|
||||
|
||||
class CatchedError extends Error {}
|
||||
|
||||
const log = dbg("app");
|
||||
|
||||
const Targets = new Map<string, typeof CompileTarget>();
|
||||
|
||||
const Targets = new Map<string,typeof CompileTarget>();
|
||||
|
||||
Targets.set("ts-esm", ESMTypescriptTarget)
|
||||
Targets.set("ts-node", NodeJSTypescriptTarget)
|
||||
|
||||
Targets.set("ts-esm", ESMTypescriptTarget);
|
||||
Targets.set("ts-node", NodeJSTypescriptTarget);
|
||||
Targets.set("c#", CSharpTarget as typeof CompileTarget);
|
||||
|
||||
function indexToLineAndCol(src: string, index: number) {
|
||||
let line = 1;
|
||||
@ -77,7 +80,11 @@ type ProcessContext = {
|
||||
processedFiles: Set<string>;
|
||||
};
|
||||
|
||||
function processFile(ctx: ProcessContext, file: string, root = false): 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);
|
||||
@ -99,7 +106,9 @@ function processFile(ctx: ProcessContext, file: string, root = false): Parsed |
|
||||
.map((statement) => {
|
||||
if (statement.type == "import") {
|
||||
const base = Path.dirname(file);
|
||||
const resolved = Path.resolve(Path.join(base, statement.path + ".jrpc"));
|
||||
const resolved = Path.resolve(
|
||||
Path.join(base, statement.path + ".jrpc")
|
||||
);
|
||||
return processFile(ctx, resolved);
|
||||
} else {
|
||||
return statement;
|
||||
@ -111,16 +120,14 @@ function processFile(ctx: ProcessContext, file: string, root = false): Parsed |
|
||||
} catch (err) {
|
||||
if (err instanceof TokenizerError) {
|
||||
printError(err, file, err.index);
|
||||
if(!root)
|
||||
throw new CatchedError();
|
||||
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) {
|
||||
if (!root) throw new CatchedError();
|
||||
} else if (root && err instanceof CatchedError) {
|
||||
return null;
|
||||
} else {
|
||||
throw err;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -132,18 +139,21 @@ export default function startCompile(options: CompileOptions) {
|
||||
} as ProcessContext;
|
||||
|
||||
let ir: IR | undefined = undefined;
|
||||
if(options.input.endsWith(".json")) {
|
||||
if (options.input.endsWith(".json")) {
|
||||
ir = JSON.parse(FS.readFileSync(options.input, "utf-8"));
|
||||
} else {
|
||||
const parsed = processFile(ctx, options.input, true);
|
||||
|
||||
if(!parsed)
|
||||
process.exit(1); // Errors should have already been emitted
|
||||
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);
|
||||
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;
|
||||
@ -151,23 +161,25 @@ export default function startCompile(options: CompileOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
if(!ir)
|
||||
throw new Error("Error compiling: Cannot get IR");
|
||||
if (!ir) throw new Error("Error compiling: Cannot get IR");
|
||||
|
||||
if(options.emitDefinitions) {
|
||||
FS.writeFileSync(options.emitDefinitions, JSON.stringify(ir, undefined, 3));
|
||||
if (options.emitDefinitions) {
|
||||
FS.writeFileSync(
|
||||
options.emitDefinitions,
|
||||
JSON.stringify(ir, undefined, 3)
|
||||
);
|
||||
}
|
||||
|
||||
if(options.targets.length <= 0) {
|
||||
if (options.targets.length <= 0) {
|
||||
console.log(Color.yellow("WARNING:"), "No targets selected!");
|
||||
}
|
||||
|
||||
options.targets.forEach(target => {
|
||||
const tg = Targets.get(target.type) as any
|
||||
if(!tg) {
|
||||
options.targets.forEach((target) => {
|
||||
const tg = Targets.get(target.type) as any;
|
||||
if (!tg) {
|
||||
console.log(Color.red("ERROR:"), "Target not supported!");
|
||||
return;
|
||||
}
|
||||
compile(ir, new tg(target.output));
|
||||
})
|
||||
compile(ir, new tg(target.output, ir.options));
|
||||
});
|
||||
}
|
||||
|
328
src/targets/csharp.ts
Normal file
328
src/targets/csharp.ts
Normal file
@ -0,0 +1,328 @@
|
||||
import {
|
||||
TypeDefinition,
|
||||
ServiceDefinition,
|
||||
EnumDefinition,
|
||||
TypeFieldDefinition,
|
||||
Step,
|
||||
} from "../ir";
|
||||
|
||||
import { CompileTarget } from "../compile";
|
||||
|
||||
type lineAppender = (ind: number, line: string | string[]) => void;
|
||||
|
||||
const conversion = {
|
||||
boolean: "bool",
|
||||
number: "double",
|
||||
string: "string",
|
||||
void: "void",
|
||||
};
|
||||
|
||||
function toCSharpType(type: string): string {
|
||||
return (conversion as any)[type] || type;
|
||||
}
|
||||
|
||||
export class CSharpTarget extends CompileTarget<{ CSharp_Namespace: string }> {
|
||||
name: string = "c#";
|
||||
|
||||
get namespace() {
|
||||
return this.options.CSharp_Namespace || "JRPC";
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.writeFile(
|
||||
this.namespace + ".csproj",
|
||||
this.getTemplate("CSharp/CSharp.csproj")
|
||||
);
|
||||
|
||||
const fixNS = (input: string) =>
|
||||
input.replace("__NAMESPACE__", this.namespace);
|
||||
const copyClass = (name: string) =>
|
||||
this.writeFile(
|
||||
name + ".cs",
|
||||
fixNS(this.getTemplate(`CSharp/${name}.cs`))
|
||||
);
|
||||
copyClass("JRpcClient");
|
||||
copyClass("JRpcServer");
|
||||
copyClass("JRpcTransport");
|
||||
}
|
||||
|
||||
generateType(definition: TypeDefinition): void {
|
||||
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, `using System.Text.Json;`);
|
||||
a(0, `using System.Text.Json.Serialization;`);
|
||||
a(0, `using System.Collections.Generic;`);
|
||||
a(0, ``);
|
||||
a(0, `namespace ${this.namespace};`);
|
||||
a(0, ``);
|
||||
a(0, `public class ${definition.name} {`);
|
||||
for (const field of definition.fields) {
|
||||
if (field.array) {
|
||||
a(
|
||||
1,
|
||||
`public IList<${toCSharpType(field.type)}>? ${
|
||||
field.name
|
||||
} { get; set; }`
|
||||
);
|
||||
} else if (field.map) {
|
||||
a(
|
||||
1,
|
||||
`public Dictionary<${toCSharpType(field.map)}, ${toCSharpType(
|
||||
field.type
|
||||
)}>? ${field.name} { get; set; }`
|
||||
);
|
||||
} else {
|
||||
a(
|
||||
1,
|
||||
`public ${toCSharpType(field.type)}? ${field.name} { get; set; }`
|
||||
);
|
||||
}
|
||||
}
|
||||
a(0, `}`);
|
||||
|
||||
this.writeFile(`${definition.name}.cs`, lines.join("\n"));
|
||||
}
|
||||
|
||||
generateEnum(definition: EnumDefinition): void {
|
||||
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, `using System.Text.Json;`);
|
||||
a(0, `using System.Text.Json.Serialization;`);
|
||||
a(0, ``);
|
||||
a(0, `namespace ${this.namespace};`);
|
||||
a(0, ``);
|
||||
a(0, `public enum ${definition.name} {`);
|
||||
for (const field of definition.values) {
|
||||
a(1, `${field.name} = ${field.value},`);
|
||||
}
|
||||
a(0, `}`);
|
||||
|
||||
this.writeFile(`${definition.name}.cs`, lines.join("\n"));
|
||||
}
|
||||
|
||||
generateServiceClient(definition: ServiceDefinition) {
|
||||
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, `using System.Text.Json;`);
|
||||
a(0, `using System.Text.Json.Serialization;`);
|
||||
a(0, `using System.Text.Json.Nodes;`);
|
||||
a(0, `using System.Threading.Tasks;`);
|
||||
a(0, ``);
|
||||
a(0, `namespace ${this.namespace};`);
|
||||
a(0, ``);
|
||||
a(0, `public class ${definition.name}Client {`);
|
||||
a(0, ``);
|
||||
a(1, `private JRpcClient Client;`);
|
||||
a(0, ``);
|
||||
a(1, `public ${definition.name}Client(JRpcClient client) {`);
|
||||
a(2, `this.Client = client;`);
|
||||
a(1, `}`);
|
||||
a(0, ``);
|
||||
for (const fnc of definition.functions) {
|
||||
let params = fnc.inputs
|
||||
.map((inp) => {
|
||||
if (inp.array) {
|
||||
return `List<${toCSharpType(inp.type)}> ${inp.name}`;
|
||||
} else {
|
||||
return `${toCSharpType(inp.type)} ${inp.name}`;
|
||||
}
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
const genParam = () =>
|
||||
a(
|
||||
2,
|
||||
`var param = new JsonArray(${fnc.inputs
|
||||
.map((e) => `JsonSerializer.SerializeToNode(${e.name})`)
|
||||
.join(", ")});`
|
||||
);
|
||||
|
||||
if (fnc.return) {
|
||||
if (fnc.return.type == "void") {
|
||||
a(1, `public async Task ${fnc.name}(${params}) {`);
|
||||
genParam();
|
||||
a(
|
||||
2,
|
||||
`await this.Client.SendRequestRaw("${definition.name}.${fnc.name}", param);`
|
||||
);
|
||||
a(1, `}`);
|
||||
} else {
|
||||
let ret = fnc.return
|
||||
? fnc.return.array
|
||||
? `IList<${toCSharpType(fnc.return.type)}>`
|
||||
: toCSharpType(fnc.return.type)
|
||||
: undefined;
|
||||
a(1, `public async Task<${ret}> ${fnc.name}(${params}) {`);
|
||||
genParam();
|
||||
a(
|
||||
2,
|
||||
`return await this.Client.SendRequest<${ret}>("${definition.name}.${fnc.name}", param);`
|
||||
);
|
||||
a(1, `}`);
|
||||
}
|
||||
} else {
|
||||
//Notification
|
||||
a(1, `public void ${fnc.name}(${params}) {`);
|
||||
genParam();
|
||||
a(
|
||||
2,
|
||||
`this.Client.SendNotification("${definition.name}.${fnc.name}", param);`
|
||||
);
|
||||
a(1, `}`);
|
||||
}
|
||||
a(1, ``);
|
||||
}
|
||||
// a(0, ``);
|
||||
// a(0, ``);
|
||||
// a(0, ``);
|
||||
a(0, `}`);
|
||||
|
||||
this.writeFile(`${definition.name}Client.cs`, lines.join("\n"));
|
||||
}
|
||||
|
||||
generateServiceServer(definition: ServiceDefinition) {
|
||||
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, `using System.Text.Json;`);
|
||||
a(0, `using System.Text.Json.Serialization;`);
|
||||
a(0, `using System.Text.Json.Nodes;`);
|
||||
a(0, `using System.Threading.Tasks;`);
|
||||
a(0, ``);
|
||||
a(0, `namespace ${this.namespace};`);
|
||||
a(0, ``);
|
||||
a(
|
||||
0,
|
||||
`public abstract class ${definition.name}Server<TContext> : JRpcService<TContext> {`
|
||||
);
|
||||
|
||||
a(0, ``);
|
||||
a(1, `public ${definition.name}Server() {`);
|
||||
for (const fnc of definition.functions) {
|
||||
a(2, `this.RegisterFunction("${fnc.name}");`);
|
||||
}
|
||||
a(1, `}`);
|
||||
a(0, ``);
|
||||
for (const fnc of definition.functions) {
|
||||
let params = [
|
||||
...fnc.inputs.map((inp) => {
|
||||
if (inp.array) {
|
||||
return `List<${toCSharpType(inp.type)}> ${inp.name}`;
|
||||
} else {
|
||||
return `${toCSharpType(inp.type)} ${inp.name}`;
|
||||
}
|
||||
}),
|
||||
"TContext ctx",
|
||||
].join(", ");
|
||||
|
||||
if (fnc.return) {
|
||||
if (fnc.return.type == "void") {
|
||||
a(1, `public abstract Task ${fnc.name}(${params});`);
|
||||
} else {
|
||||
let ret = fnc.return
|
||||
? fnc.return.array
|
||||
? `IList<${toCSharpType(fnc.return.type)}>`
|
||||
: toCSharpType(fnc.return.type)
|
||||
: undefined;
|
||||
a(1, `public abstract Task<${ret}> ${fnc.name}(${params});`);
|
||||
}
|
||||
} else {
|
||||
a(1, `public abstract void ${fnc.name}(${params});`);
|
||||
}
|
||||
}
|
||||
a(0, ``);
|
||||
a(
|
||||
1,
|
||||
`public async override Task<JsonNode?> HandleRequest(string func, JsonNode param, TContext context) {`
|
||||
);
|
||||
a(2, `switch(func) {`);
|
||||
for (const fnc of definition.functions) {
|
||||
a(3, `case "${fnc.name}": {`);
|
||||
a(4, `if(param is JsonObject) {`);
|
||||
a(
|
||||
5,
|
||||
`var ja = new JsonArray(${fnc.inputs
|
||||
.map((inp) => {
|
||||
return `param["${inp.name}"]`;
|
||||
})
|
||||
.join(", ")});`
|
||||
);
|
||||
a(5, `param = ja;`);
|
||||
a(4, `}`);
|
||||
|
||||
let pref = "";
|
||||
if (fnc.return) {
|
||||
if (fnc.return.type != "void") pref = "var result = await ";
|
||||
else pref = "await ";
|
||||
}
|
||||
|
||||
a(
|
||||
4,
|
||||
pref +
|
||||
`this.${fnc.name}(${[
|
||||
...fnc.inputs.map((inp, idx) => {
|
||||
let type = inp.array
|
||||
? `List<${toCSharpType(inp.type)}>`
|
||||
: `${toCSharpType(inp.type)}`;
|
||||
return `param[${idx}]!.Deserialize<${type}>()`;
|
||||
}),
|
||||
"context",
|
||||
].join(", ")});`
|
||||
);
|
||||
|
||||
if (fnc.return && fnc.return.type != "void") {
|
||||
// if(fnc.return.type == "void") {
|
||||
// a(3, `return null;`);
|
||||
// } else {
|
||||
// a(3, ``);
|
||||
// }
|
||||
a(4, `return JsonSerializer.SerializeToNode(result);`);
|
||||
// a(3, ``);
|
||||
} else {
|
||||
a(4, `return null;`);
|
||||
}
|
||||
a(3, `}`);
|
||||
a(0, ``);
|
||||
}
|
||||
a(3, `default:`);
|
||||
a(4, `throw new Exception("Invalid Method!");`);
|
||||
// a(0, ``);
|
||||
// a(0, ``);
|
||||
// a(0, ``);
|
||||
a(2, `}`);
|
||||
a(1, `}`);
|
||||
a(0, `}`);
|
||||
|
||||
this.writeFile(`${definition.name}Server.cs`, lines.join("\n"));
|
||||
}
|
||||
|
||||
generateService(definition: ServiceDefinition): void {
|
||||
this.generateServiceClient(definition);
|
||||
this.generateServiceServer(definition);
|
||||
}
|
||||
|
||||
finalize(steps: Step[]): void {}
|
||||
}
|
@ -57,7 +57,7 @@ const matcher = [
|
||||
regexMatcher(/^#.+/, "comment"),
|
||||
regexMatcher(/^".*?"/, "string"),
|
||||
// regexMatcher(/(?<=^")(.*?)(?=")/, "string"),
|
||||
regexMatcher(/^(type|enum|import|service)\b/, "keyword"),
|
||||
regexMatcher(/^(type|enum|import|service|define)\b/, "keyword"),
|
||||
regexMatcher(/^\@/, "at"),
|
||||
regexMatcher(/^\:/, "colon"),
|
||||
regexMatcher(/^\;/, "semicolon"),
|
||||
|
Reference in New Issue
Block a user