diff --git a/.gitignore b/.gitignore index 5844ea4..c46a5d8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ examples/CSharp/Generated examples/CSharp/Example/bin examples/CSharp/Example/obj examples/definition.json +examples/Rust/Generated templates/CSharp/bin templates/CSharp/obj diff --git a/examples/example.jrpc b/examples/example.jrpc index cecfccf..f555267 100644 --- a/examples/example.jrpc +++ b/examples/example.jrpc @@ -1,6 +1,7 @@ import "./import"; define csharp_namespace Example; +define rust_crate example; enum TestEnum { VAL1, @@ -14,17 +15,17 @@ type Test { atom: TestAtom; array: TestAtom[]; enumValue: TestEnum; - map: {number, TestAtom}; + map: {int, TestAtom}; } type AddValueRequest { - value1: number; - value2: number; + value1: float; + value2: float; } type AddValueResponse { - value: number; + value: float; } service TestService { @@ -36,11 +37,11 @@ service TestService { @Description("Add two numbers") @Param("value1", "The first value") @Param("value2", "The second value") - AddValuesMultipleParams(value1: number, value2: number): number; + AddValuesMultipleParams(value1: float, value2: float): float; @Description("Does literaly nothing") @Param("param1", "Some number") - ReturningVoid(param1: number): void; + ReturningVoid(param1: float): void; @Description("Just sends an Event with a String") @Param("param1", "Parameter with some string for event") @@ -49,5 +50,5 @@ service TestService { ThrowingError(): void; - FunctionWithArrayAsParamAndReturn(values1: number[], values2: number[]): number[]; + FunctionWithArrayAsParamAndReturn(values1: float[], values2: float[]): float[]; } diff --git a/examples/import.jrpc b/examples/import.jrpc index 58196ff..e44c177 100644 --- a/examples/import.jrpc +++ b/examples/import.jrpc @@ -1,5 +1,5 @@ type TestAtom { - val_number: number; + val_number: float; val_boolean: boolean; val_string: string; } diff --git a/lib/jrpc.js b/lib/jrpc.js index 99aa043..cefdc1d 100755 --- a/lib/jrpc.js +++ b/lib/jrpc.js @@ -1656,7 +1656,7 @@ var require_route = __commonJS({ } module2.exports = function(fromModel) { const graph = deriveBFS(fromModel); - const conversion3 = {}; + const conversion4 = {}; const models = Object.keys(graph); for (let len = models.length, i = 0; i < len; i++) { const toModel = models[i]; @@ -1664,9 +1664,9 @@ var require_route = __commonJS({ if (node.parent === null) { continue; } - conversion3[toModel] = wrapConversion(toModel, graph); + conversion4[toModel] = wrapConversion(toModel, graph); } - return conversion3; + return conversion4; }; } }); @@ -9876,7 +9876,7 @@ function parse(tokens, file) { // src/ir.ts var import_debug = __toESM(require_src()); var log = (0, import_debug.default)("app"); -var BUILTIN = ["number", "string", "boolean"]; +var BUILTIN = ["float", "int", "string", "boolean"]; var IRError = class extends Error { constructor(statement, message) { super("Error building IR: " + message); @@ -9912,7 +9912,7 @@ function get_ir(parsed) { if (depends.indexOf(field.fieldtype) < 0) depends.push(field.fieldtype); } - if (field.map && field.map !== "number" && field.map !== "string") { + if (field.map && field.map !== "int" && field.map !== "string") { throw new IRError(field, `Type ${field.map} is not valid as map key!`); } return { @@ -10047,6 +10047,7 @@ function get_ir(parsed) { } else if (statement.type == "define") { options[statement.key] = statement.value; if ((statement.key == "use_messagepack" || statement.key == "allow_bytes") && statement.value == "true") { + options["allow_bytes"] = true; builtin.push("bytes"); } } else { @@ -10074,10 +10075,14 @@ var CompileTarget = class { } } writeFile(name, content) { + let resPath = Path.join(this.outputFolder, name); + let resDir = Path.dirname(resPath); + if (!FS.existsSync(resDir)) + FS.mkdirSync(resDir, { recursive: true }); if (content instanceof Promise) { - content.then((res) => FS.writeFileSync(Path.join(this.outputFolder, name), res)); + content.then((res) => FS.writeFileSync(resPath, res)); } else { - FS.writeFileSync(Path.join(this.outputFolder, name), content); + FS.writeFileSync(resPath, content); } } getTemplate(name) { @@ -10120,7 +10125,8 @@ function compile(ir, target) { // src/targets/typescript.ts var conversion = { boolean: "boolean", - number: "number", + int: "number", + float: "number", string: "string", void: "void", bytes: "Uint8Array" @@ -10139,7 +10145,7 @@ var TypescriptTarget = class extends CompileTarget { `; } generateImports(a, def) { - a(0, this.generateImport(`{ VerificationError, apply_number, apply_string, apply_boolean, apply_void }`, `./ts_base`)); + a(0, this.generateImport(`{ VerificationError, apply_int, apply_float, apply_string, apply_boolean, apply_void }`, `./ts_base`)); a(0, def.depends.map((dep) => this.generateImport(`${dep}, { apply_${dep} }`, "./" + dep))); } getFileName(typename) { @@ -10440,7 +10446,8 @@ var NodeJSTypescriptTarget = class extends TypescriptTarget { // src/targets/csharp.ts var conversion2 = { boolean: "bool", - number: "double", + int: "long", + float: "double", string: "string", void: "void", bytes: "" @@ -10454,8 +10461,8 @@ var CSharpTarget = class extends CompileTarget { return this.options.csharp_namespace || "JRPC"; } start() { - if (this.options.use_messagepack == true) { - throw new Error("C# has no support for MessagePack yet!"); + if (this.options.allow_bytes == true) { + throw new Error("C# has no support for 'bytes' yet!"); } this.writeFile(this.namespace + ".csproj", this.getTemplate("CSharp/CSharp.csproj")); const fixNS = (input) => input.replace("__NAMESPACE__", this.namespace); @@ -10670,6 +10677,111 @@ var CSharpTarget = class extends CompileTarget { } }; +// src/targets/rust.ts +var conversion3 = { + boolean: "bool", + float: "f64", + int: "i64", + string: "String", + void: "void", + bytes: "Vec" +}; +function toRustType(type) { + return conversion3[type] || type; +} +function toSnake(input) { + return input[0].toLowerCase() + input.slice(1).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +} +var RustTarget = class extends CompileTarget { + name = "rust"; + get crate() { + return this.options.rust_crate; + } + start() { + if (!this.crate) + throw new Error("Setting a crate name is required. Add the following to your jrpc file: 'define rust_crate '"); + if (this.options.allow_bytes == true) { + throw new Error("Rust has no support for 'bytes' yet!"); + } + this.writeFile("Cargo.toml", this.getTemplate("Rust/Cargo.toml").replace("${NAME}", this.crate)); + } + addDependencies(a, def) { + for (const dep of def.depends) { + a(0, `use crate::${dep};`); + } + a(0, ``); + a(0, ``); + } + generateType(definition) { + let lines = []; + const a = (i, t) => { + if (!Array.isArray(t)) { + t = [t]; + } + t.forEach((l) => lines.push(" ".repeat(i) + l.trim())); + }; + if (definition.fields.find((e) => e.map)) + a(0, `use std::collections::hash_map::HashMap;`); + a(0, `use serde::{Deserialize, Serialize};`); + this.addDependencies(a, definition); + a(0, `#[derive(Clone, Debug, Serialize, Deserialize)]`); + a(0, `pub struct ${definition.name} {`); + for (const field of definition.fields) { + if (field.array) { + a(1, `pub ${field.name}: Vec<${toRustType(field.type)}>,`); + } else if (field.map) { + a(1, `pub ${field.name}: HashMap<${toRustType(field.map)}, ${toRustType(field.type)}>,`); + } else { + a(1, `pub ${field.name}: ${toRustType(field.type)},`); + } + } + a(0, `}`); + this.writeFile(`src/${toSnake(definition.name)}.rs`, lines.join("\n")); + } + generateEnum(definition) { + let lines = []; + const a = (i, t) => { + if (!Array.isArray(t)) { + t = [t]; + } + t.forEach((l) => lines.push(" ".repeat(i) + l.trim())); + }; + a(0, `use int_enum::IntEnum;`); + a(0, `use serde::{Deserialize, Serialize};`); + a(0, ``); + a(0, ``); + a(0, `#[repr(i64)]`); + a(0, "#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum, Deserialize, Serialize)]"); + a(0, `pub enum ${definition.name} {`); + for (const field of definition.values) { + a(1, `${field.name} = ${field.value},`); + } + a(0, `}`); + this.writeFile(`src/${toSnake(definition.name)}.rs`, lines.join("\n")); + } + generateService(definition) { + } + generateLib(steps) { + let lines = []; + const a = (i, t) => { + if (!Array.isArray(t)) { + t = [t]; + } + t.forEach((l) => lines.push(" ".repeat(i) + l.trim())); + }; + for (const [typ, def] of steps) { + if (typ == "type" || typ == "enum") { + a(0, `mod ${toSnake(def.name)};`); + a(0, `pub use ${toSnake(def.name)}::${def.name};`); + } + } + this.writeFile(`src/lib.rs`, lines.join("\n")); + } + finalize(steps) { + this.generateLib(steps); + } +}; + // src/process.ts var CatchedError = class extends Error { }; @@ -10678,6 +10790,7 @@ var Targets = /* @__PURE__ */ new Map(); Targets.set("ts-esm", ESMTypescriptTarget); Targets.set("ts-node", NodeJSTypescriptTarget); Targets.set("c#", CSharpTarget); +Targets.set("rust", RustTarget); function indexToLineAndCol(src, index) { let line = 1; let col = 1; diff --git a/package.json b/package.json index 2a2108d..28900bd 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "@hibas123/jrpcgen", - "version": "1.0.31", + "version": "1.1.0", "main": "lib/index.js", "license": "MIT", "packageManager": "yarn@3.1.1", "scripts": { "start": "ts-node src/index.ts", - "test-start": "npm run start -- compile examples/example.jrpc --definition=examples/definition.json -o=ts-node:examples/Typescript/out -o=c#:examples/CSharp/Generated", + "test-start": "npm run start -- compile examples/example.jrpc --definition=examples/definition.json -o=ts-node:examples/Typescript/out -o=c#:examples/CSharp/Generated -o=rust:examples/Rust/Generated", "test-csharp": "cd examples/CSharp/Example/ && dotnet run", + "test-rust": "cd examples/Rust/Generated/ && cargo build", "test-typescript": "ts-node examples/test.ts", "test": "npm run test-start && npm run test-csharp && npm run test-typescript", "build": "esbuild src/index.ts --bundle --platform=node --target=node14 --outfile=lib/jrpc.js", @@ -19,7 +20,7 @@ "files": [ "lib/jrpc.js", "templates/**", - "examples/**", + "examples/*.jrpc", "src/**", "tsconfig.json" ], diff --git a/src/compile.ts b/src/compile.ts index 76193a9..07f0be3 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -1,5 +1,5 @@ import * as FS from "fs"; -import * as FSE from "fs-extra" +import * as FSE from "fs-extra"; import * as Path from "path"; import { EnumDefinition, @@ -11,7 +11,10 @@ import { export abstract class CompileTarget { abstract name: string; - constructor(private outputFolder: string, protected options: T & { use_messagepack: boolean }) { + constructor( + private outputFolder: string, + protected options: T & { allow_bytes: boolean } + ) { if (!FS.existsSync(outputFolder)) { FS.mkdirSync(outputFolder, { recursive: true, @@ -30,12 +33,13 @@ export abstract class CompileTarget { abstract finalize(steps: Step[]): void; protected writeFile(name: string, content: string | Promise) { + let resPath = Path.join(this.outputFolder, name); + let resDir = Path.dirname(resPath); + if (!FS.existsSync(resDir)) FS.mkdirSync(resDir, { recursive: true }); if (content instanceof Promise) { - content.then((res) => - FS.writeFileSync(Path.join(this.outputFolder, name), res) - ); + content.then((res) => FS.writeFileSync(resPath, res)); } else { - FS.writeFileSync(Path.join(this.outputFolder, name), content); + FS.writeFileSync(resPath, content); } } @@ -59,12 +63,10 @@ export abstract class CompileTarget { return res.join("\n"); } - protected loadTemplateFolder(name:string) { + protected loadTemplateFolder(name: string) { let root = Path.join(__dirname, "../templates/", name); - - FSE.copySync(root, this.outputFolder, { - }); + FSE.copySync(root, this.outputFolder, {}); } } diff --git a/src/ir.ts b/src/ir.ts index 3fccf48..b360796 100644 --- a/src/ir.ts +++ b/src/ir.ts @@ -2,7 +2,7 @@ import type { Parsed, StatementNode } from "./parser"; import dbg from "debug"; const log = dbg("app"); -const BUILTIN = ["number", "string", "boolean"]; +const BUILTIN = ["float", "int", "string", "boolean"]; export class IRError extends Error { constructor(public statement: StatementNode, message: string) { @@ -115,7 +115,7 @@ export default function get_ir(parsed: Parsed): IR { depends.push(field.fieldtype); } - if (field.map && field.map !== "number" && field.map !== "string") { + if (field.map && field.map !== "int" && field.map !== "string") { throw new IRError( field, `Type ${field.map} is not valid as map key!` @@ -312,6 +312,7 @@ export default function get_ir(parsed: Parsed): IR { statement.key == "allow_bytes") && statement.value == "true" ) { + options["allow_bytes"] = true; builtin.push("bytes"); } } else { diff --git a/src/process.ts b/src/process.ts index d8cc255..42ade9f 100644 --- a/src/process.ts +++ b/src/process.ts @@ -11,6 +11,7 @@ import { NodeJSTypescriptTarget, } from "./targets/typescript"; import { CSharpTarget } from "./targets/csharp"; +import { RustTarget } from "./targets/rust"; class CatchedError extends Error {} @@ -21,7 +22,7 @@ export const Targets = new Map(); Targets.set("ts-esm", ESMTypescriptTarget); Targets.set("ts-node", NodeJSTypescriptTarget); Targets.set("c#", CSharpTarget as typeof CompileTarget); - +Targets.set("rust", RustTarget as typeof CompileTarget); function indexToLineAndCol(src: string, index: number) { let line = 1; diff --git a/src/targets/csharp.ts b/src/targets/csharp.ts index c30b154..82e75ae 100644 --- a/src/targets/csharp.ts +++ b/src/targets/csharp.ts @@ -12,10 +12,11 @@ type lineAppender = (ind: number, line: string | string[]) => void; const conversion = { boolean: "bool", - number: "double", + int: "long", + float: "double", string: "string", void: "void", - bytes: "" + bytes: "", }; function toCSharpType(type: string): string { @@ -30,8 +31,8 @@ export class CSharpTarget extends CompileTarget<{ csharp_namespace: string }> { } start(): void { - if(this.options.use_messagepack == true) { - throw new Error("C# has no support for MessagePack yet!"); + if (this.options.allow_bytes == true) { + throw new Error("C# has no support for 'bytes' yet!"); } this.writeFile( this.namespace + ".csproj", diff --git a/src/targets/rust.ts b/src/targets/rust.ts new file mode 100644 index 0000000..c468e6e --- /dev/null +++ b/src/targets/rust.ts @@ -0,0 +1,151 @@ +import { CompileTarget } from "../compile"; +import { TypeDefinition, EnumDefinition, ServiceDefinition, Step } from "../ir"; + +type lineAppender = (ind: number, line: string | string[]) => void; + +const conversion = { + boolean: "bool", + float: "f64", + int: "i64", + string: "String", + void: "void", + bytes: "Vec", +}; + +function toRustType(type: string): string { + return (conversion as any)[type] || type; +} + +function toSnake(input: string) { + return ( + input[0].toLowerCase() + + input.slice(1).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) + ); +} + +export class RustTarget extends CompileTarget<{ rust_crate: string }> { + name: string = "rust"; + + get crate() { + return this.options.rust_crate; + } + + start(): void { + if (!this.crate) + throw new Error( + "Setting a crate name is required. Add the following to your jrpc file: 'define rust_crate '" + ); + + if (this.options.allow_bytes == true) { + throw new Error("Rust has no support for 'bytes' yet!"); + } + + this.writeFile( + "Cargo.toml", + this.getTemplate("Rust/Cargo.toml").replace("${NAME}", this.crate) + ); + } + + private addDependencies( + a: lineAppender, + def: TypeDefinition | ServiceDefinition + ) { + for (const dep of def.depends) { + a(0, `use crate::${dep};`); + } + + a(0, ``); + a(0, ``); + } + + 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())); + }; + if (definition.fields.find((e) => e.map)) + a(0, `use std::collections::hash_map::HashMap;`); + a(0, `use serde::{Deserialize, Serialize};`); + this.addDependencies(a, definition); + + a(0, `#[derive(Clone, Debug, Serialize, Deserialize)]`); + a(0, `pub struct ${definition.name} {`); + for (const field of definition.fields) { + if (field.array) { + a(1, `pub ${field.name}: Vec<${toRustType(field.type)}>,`); + } else if (field.map) { + a( + 1, + `pub ${field.name}: HashMap<${toRustType( + field.map + )}, ${toRustType(field.type)}>,` + ); + } else { + a(1, `pub ${field.name}: ${toRustType(field.type)},`); + } + } + a(0, `}`); + + this.writeFile(`src/${toSnake(definition.name)}.rs`, 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, `use int_enum::IntEnum;`); + a(0, `use serde::{Deserialize, Serialize};`); + + a(0, ``); + a(0, ``); + + a(0, `#[repr(i64)]`); + a( + 0, + "#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum, Deserialize, Serialize)]" + ); + a(0, `pub enum ${definition.name} {`); + for (const field of definition.values) { + a(1, `${field.name} = ${field.value},`); + } + a(0, `}`); + + this.writeFile(`src/${toSnake(definition.name)}.rs`, lines.join("\n")); + } + + generateService(definition: ServiceDefinition): void { + // throw new Error("Service not implemented."); + } + + private generateLib(steps: Step[]) { + let lines: string[] = []; + const a: lineAppender = (i, t) => { + if (!Array.isArray(t)) { + t = [t]; + } + t.forEach((l) => lines.push(" ".repeat(i) + l.trim())); + }; + + for (const [typ, def] of steps) { + if (typ == "type" || typ == "enum") { + a(0, `mod ${toSnake(def.name)};`); + a(0, `pub use ${toSnake(def.name)}::${def.name};`); + } + } + + this.writeFile(`src/lib.rs`, lines.join("\n")); + } + + finalize(steps: Step[]): void { + this.generateLib(steps); + // throw new Error("Method not implemented."); + } +} diff --git a/src/targets/typescript.ts b/src/targets/typescript.ts index a8103d0..9dbcb71 100644 --- a/src/targets/typescript.ts +++ b/src/targets/typescript.ts @@ -13,7 +13,8 @@ type lineAppender = (ind: number, line: string | string[]) => void; const conversion = { boolean: "boolean", - number: "number", + int: "number", + float: "number", string: "string", void: "void", bytes: "Uint8Array", @@ -45,7 +46,7 @@ export class TypescriptTarget extends CompileTarget { a( 0, this.generateImport( - `{ VerificationError, apply_number, apply_string, apply_boolean, apply_void }`, + `{ VerificationError, apply_int, apply_float, apply_string, apply_boolean, apply_void }`, `./ts_base` ) ); diff --git a/templates/Rust/Cargo.toml b/templates/Rust/Cargo.toml new file mode 100644 index 0000000..d775c00 --- /dev/null +++ b/templates/Rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "${NAME}" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +int-enum = "0.4.0" +serde = { version = "1.0.136", features = ["derive"] } + +# These might not have to be included, since the serialization could be implementation dependent and having a dependency less is always a good thing. +# serde_json = "1.0.79" +# rmp-serde = "1.0.0" diff --git a/templates/ts_base.ts b/templates/ts_base.ts index e94ca58..7c27aa2 100644 --- a/templates/ts_base.ts +++ b/templates/ts_base.ts @@ -4,13 +4,24 @@ export class VerificationError extends Error { public readonly field?: string, public readonly value?: any ) { - super("Parameter verification failed! " +(type ? "Expected " + type + "! " :"") + (field ? "At: " + field + "! " : "")); + super( + "Parameter verification failed! " + + (type ? "Expected " + type + "! " : "") + + (field ? "At: " + field + "! " : "") + ); } } -export function apply_number(data: any) { +export function apply_int(data: any) { + data = Math.floor(Number(data)); + if (Number.isNaN(data)) throw new VerificationError("int", undefined, data); + return data; +} + +export function apply_float(data: any) { data = Number(data); - if(Number.isNaN(data)) throw new VerificationError("number", undefined, data); + if (Number.isNaN(data)) + throw new VerificationError("float", undefined, data); return data; }