diff --git a/.gitignore b/.gitignore index 5844ea4..a275b06 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,7 @@ examples/CSharp/Generated examples/CSharp/Example/bin examples/CSharp/Example/obj examples/definition.json +examples/Rust/Gen +examples/Rust/Impl/target templates/CSharp/bin templates/CSharp/obj diff --git a/examples/Rust/.gitignore b/examples/Rust/.gitignore new file mode 100644 index 0000000..8b88752 --- /dev/null +++ b/examples/Rust/.gitignore @@ -0,0 +1,2 @@ +Gen/ +Impl/target diff --git a/examples/Rust/Impl/Cargo.lock b/examples/Rust/Impl/Cargo.lock new file mode 100644 index 0000000..1c995a6 --- /dev/null +++ b/examples/Rust/Impl/Cargo.lock @@ -0,0 +1,248 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "int-enum" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b1428b2b1abe959e6eedb0a17d0ab12f6ba20e1106cc29fc4874e3ba393c177" +dependencies = [ + "cfg-if 0.1.10", + "int-enum-impl", +] + +[[package]] +name = "int-enum-impl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c3cecaad8ca1a5020843500c696de2b9a07b63b624ddeef91f85f9bafb3671" +dependencies = [ + "cfg-if 0.1.10", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "libc" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "test" +version = "0.1.0" +dependencies = [ + "int-enum", + "nanoid", + "serde", + "serde_json", + "threadpool", +] + +[[package]] +name = "test-impl" +version = "0.1.0" +dependencies = [ + "test", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/examples/Rust/Impl/Cargo.toml b/examples/Rust/Impl/Cargo.toml new file mode 100644 index 0000000..6f2be4e --- /dev/null +++ b/examples/Rust/Impl/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "test-impl" +version = "0.1.0" +edition = "2021" + +[dependencies] +test = { path = "../Gen/" } diff --git a/examples/Rust/Impl/src/main.rs b/examples/Rust/Impl/src/main.rs new file mode 100644 index 0000000..02a1e23 --- /dev/null +++ b/examples/Rust/Impl/src/main.rs @@ -0,0 +1,52 @@ +use test::{JRPCServer,Result,Test}; +use test::server::{TestService,TestServiceHandler}; +use std::io::{BufReader,BufRead,Write}; +use std::sync::mpsc::channel; + +#[derive(Clone, Copy)] +struct MyCtx {} + +struct TestServiceImplementation {} + +impl TestService for TestServiceImplementation { + fn GetTest(&self, name: String, age: i64, _context: &MyCtx) -> Result { + return Ok(Test { name, age }); + } + + fn TestNot(&self, _: &MyCtx) -> Result<()> { + return Ok(()); + } +} + +pub fn main() { + let mut srv = JRPCServer::::new(); + srv.add_service(TestServiceHandler::new(Box::from(TestServiceImplementation {}))); + + let listener = std::net::TcpListener::bind("127.0.0.1:4321").expect("Could not start listener!"); + + for stream in listener.incoming() { + println!("Got Connection!"); + let (stx, srx) = channel::(); + let (rtx, rrx) = channel::(); + + let mut stream = stream.expect("Bad stream"); + let mut r = BufReader::new(stream.try_clone().unwrap()); + let mut line = String::new(); + + srv.start_session(srx, rtx, MyCtx {}); + + + r.read_line(&mut line).expect("Could not read line!"); + println!("Got line: {}", line); + stx.send(line).expect("Could not send packet to handler!"); + println!("Sending to handler succeeded"); + let response = rrx.recv().expect("Could not get reponse from handler!"); + println!("Prepared response {}", response); + + stream.write((response + "\n").as_bytes()).expect("Could not send reponse!"); + } + + println!("Hello World"); + + // return Ok(()); +} diff --git a/examples/test.jrpc b/examples/test.jrpc new file mode 100644 index 0000000..af947c5 --- /dev/null +++ b/examples/test.jrpc @@ -0,0 +1,14 @@ +define rust_crate test; + +type Test { + name: string; + age: int; +} + +service TestService { + @Description("asdasdasd") + GetTest(name: string, age: int): Test; + notification TestNot(); +} + +// { "jsonrpc": "2.0", "method": "TestService.GetTest", "params": [ "SomeName", 25 ], "id": "someid" } diff --git a/lib/jrpc.js b/lib/jrpc.js index 455fa1a..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; }; } }); @@ -10677,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 { }; @@ -10685,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 9b8afee..66f37fb 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,10 @@ "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-typescript": "cd examples/Typescript && ts-node test.ts", + "test-rust": "cd examples/Rust/Generated/ && cargo build", "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", "prepublishOnly": "npm run build" diff --git a/src/process.ts b/src/process.ts index 3140445..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,6 +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/rust.ts b/src/targets/rust.ts new file mode 100644 index 0000000..c32d75f --- /dev/null +++ b/src/targets/rust.ts @@ -0,0 +1,377 @@ +import chalk from "chalk"; +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: "()", + 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) + ); + + this.writeFile("src/base_lib.rs", this.getTemplate("Rust/src/lib.rs")); + } + + 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")); + } + + private generateServiceClient(definition: ServiceDefinition): void { + let lines: string[] = []; + const a: lineAppender = (i, t) => { + if (!Array.isArray(t)) { + t = [t]; + } + t.forEach((l) => lines.push(" ".repeat(i) + l.trim())); + }; + + const typeToRust = (type: string, array: boolean) => { + let rt = toRustType(type); + return array ? `Vec<${rt}>` : rt; + }; + + this.addDependencies(a, definition); + + a(0, `use crate::base_lib::{JRPCClient,JRPCRequest,Result};`); + a(0, `use serde_json::{json};`); + a(0, ``); + a(0, `pub struct ${definition.name}{`); + a(1, `client: JRPCClient,`); + a(0, `}`); + a(0, ``); + + a(0, `impl ${definition.name} {`); + a(1, `pub fn new(client: JRPCClient) -> Self {`); + a(2, `return Self { client };`); + a(1, `}`); + a(0, ``); + for (const fnc of definition.functions) { + let params = fnc.inputs + .map((i) => i.name + ": " + typeToRust(i.type, i.array)) + .join(", "); + let ret = fnc.return + ? typeToRust(fnc.return.type, fnc.return.array) + : "()"; + a(1, `pub fn ${fnc.name}(&self, ${params}) -> Result<${ret}> {`); + a(2, `let l_req = JRPCRequest {`); + a(3, `jsonrpc: "2.0".to_owned(),`); + a(3, `id: None, // 'id' will be set by the send_request function`); + a(3, `method: "${definition.name}.${fnc.name}".to_owned(),`); + a(3, `params: json!([${fnc.inputs.map((e) => e.name)}])`); + a(2, `};`); + a(2, ``); + if (fnc.return) { + a(2, `let l_res = self.client.send_request(l_req);`); + a(2, `if let Err(e) = l_res {`); + a(3, `return Err(e);`); + a(2, `} else if let Ok(o) = l_res {`); + if (fnc.return.type == "void") { + a(3, `return ();`); + } else { + a( + 3, + `return serde_json::from_value(o).map_err(|e| Box::from(e));` + ); + } + a(2, `} else { panic!("What else cases could there be?"); }`); + } else { + a(2, `self.client.send_notification(l_req);`); + a(2, `return Ok(());`); + } + a(1, `}`); + } + a(0, `}`); + a(0, ``); + + this.writeFile( + `src/client/${toSnake(definition.name)}.rs`, + lines.join("\n") + ); + } + + private generateServiceServer(definition: ServiceDefinition): void { + let lines: string[] = []; + const a: lineAppender = (i, t) => { + if (!Array.isArray(t)) { + t = [t]; + } + t.forEach((l) => lines.push(" ".repeat(i) + l.trim())); + }; + + this.addDependencies(a, definition); + a(0, `use crate::base_lib::{JRPCServiceHandler,JRPCRequest,Result};`); + a(0, `use serde_json::{Value};`); + + const typeToRust = (type: string, array: boolean) => { + let rt = toRustType(type); + return array ? `Vec<${rt}>` : rt; + }; + + const CTX_TYPE = "'static + Sync + Send + Copy"; + + a(0, ``); + a(0, `pub trait ${definition.name} {`); + for (const fnc of definition.functions) { + let params = + fnc.inputs.length > 0 + ? fnc.inputs + .map((i) => i.name + ": " + typeToRust(i.type, i.array)) + .join(", ") + ", " + : ""; + let ret = fnc.return + ? typeToRust(fnc.return.type, fnc.return.array) + : "()"; + a(1, `fn ${fnc.name}(&self, ${params}context: &C) -> Result<${ret}>;`); + } + a(0, `}`); + + a(0, ``); + a(0, `pub struct ${definition.name}Handler {`); + a(1, `implementation: Box + Sync + Send>,`); + a(0, `}`); + a(0, ``); + a(0, `impl ${definition.name}Handler {`); + a( + 1, + `pub fn new(implementation: Box + Sync + Send>) -> Box {` + ); + a(2, `return Box::from(Self { implementation });`); + a(1, `}`); + a(0, `}`); + + a(0, ``); + + a( + 0, + `impl JRPCServiceHandler for ${definition.name}Handler {` + ); + + a(1, `fn get_name(&self) -> String { "${definition.name}".to_owned() }`); + a(0, ``); + + a( + 1, + `fn on_message(&self, msg: JRPCRequest, function: String, ctx: &C) -> Result<(bool, Value)> {` + ); + a(2, `match function.as_str() {`); + for (const fnc of definition.functions) { + a(3, `"${fnc.name}" => {`); + a(4, `if msg.params.is_array() {`); + if (fnc.inputs.length > 0) { + a( + 5, + `let arr = msg.params.as_array().unwrap(); //TODO: Check if this can fail.` + ); + } + a(5, `let res = self.implementation.${fnc.name}(`); + for (let i = 0; i < fnc.inputs.length; i++) { + const inp = fnc.inputs[i]; + a(6, `serde_json::from_value(arr[${i}].clone())`); + a( + 7, + `.map_err(|_| "Parameter for field '${inp.name}' should be of type '${inp.type}'!")?,` //TODO: Array + ); + } + a(5, `ctx)?;`); + if (fnc.return) { + a(5, `return Ok((true, serde_json::to_value(res)?));`); + } else { + a(5, `_ = res;`); + a(5, `return Ok((false, Value::Null))`); + } + a(4, `} else if msg.params.is_object() {`); + a(5, `return Err(Box::from("Not implemented yet".to_owned()));`); + a(4, `} else {`); + a(5, `return Err(Box::from("Invalid parameters??".to_owned()));`); + a(4, `}`); + a(3, `},`); + } + a( + 3, + `_ => { return Err(Box::from(format!("Invalid function {}", function).to_owned())) ;},` + ); + a(2, `}`); + a(1, `}`); + a(0, `}`); + this.writeFile( + `src/server/${toSnake(definition.name)}.rs`, + lines.join("\n") + ); + } + + generateService(definition: ServiceDefinition): void { + console.log( + chalk.yellow("WARNING:"), + "Rust support for services is WIP. Use with care!" + ); + this.generateServiceServer(definition); + this.generateServiceClient(definition); + } + + 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())); + }; + + let linesServer: string[] = []; + const as: lineAppender = (i, t) => { + if (!Array.isArray(t)) { + t = [t]; + } + t.forEach((l) => linesServer.push(" ".repeat(i) + l.trim())); + }; + + let linesClient: string[] = []; + const ac: lineAppender = (i, t) => { + if (!Array.isArray(t)) { + t = [t]; + } + t.forEach((l) => linesClient.push(" ".repeat(i) + l.trim())); + }; + + a(0, `pub mod base_lib;`); + a(0, `pub use base_lib::{JRPCServer,Result};`); + + 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};`); + } else if (typ == "service") { + as(0, `mod ${toSnake(def.name)};`); + as( + 0, + `pub use ${toSnake(def.name)}::{${def.name}, ${ + def.name + }Handler};` + ); + + ac(0, `mod ${toSnake(def.name)};`); + ac(0, `pub use ${toSnake(def.name)}::${def.name};`); + + a(0, `pub mod server;`); + a(0, `pub mod client;`); + } + } + + this.writeFile(`src/lib.rs`, lines.join("\n")); + this.writeFile(`src/server/mod.rs`, linesServer.join("\n")); + this.writeFile(`src/client/mod.rs`, linesClient.join("\n")); + } + + finalize(steps: Step[]): void { + this.generateLib(steps); + // throw new Error("Method not implemented."); + } +} diff --git a/templates/Rust/.gitignore b/templates/Rust/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/templates/Rust/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/templates/Rust/Cargo.toml b/templates/Rust/Cargo.toml new file mode 100644 index 0000000..dde49f3 --- /dev/null +++ b/templates/Rust/Cargo.toml @@ -0,0 +1,15 @@ +[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"] } +serde_json = "1.0.79" +threadpool = "1.8.1" +nanoid = "0.4.0" + + diff --git a/templates/Rust/src/lib.rs b/templates/Rust/src/lib.rs new file mode 100644 index 0000000..5754855 --- /dev/null +++ b/templates/Rust/src/lib.rs @@ -0,0 +1,286 @@ +use nanoid::nanoid; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::boxed::Box; +use std::collections::HashMap; +use std::error::Error; +use std::marker::PhantomData; +use std::marker::Send; +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::{Arc, Mutex}; +use threadpool::ThreadPool; + +pub type Result = std::result::Result>; + +// TODO: Check what happens when error code is not included +// #[repr(i64)] +// #[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum, Deserialize, Serialize)] +// pub enum ErrorCodes { +// ParseError = -32700, +// InvalidRequest = -32600, +// MethodNotFound = -32601, +// InvalidParams = -32602, +// InternalError = -32603, +// } + +#[derive(Serialize, Deserialize)] +pub struct JRPCRequest { + pub jsonrpc: String, + pub id: Option, + pub method: String, + pub params: Value, +} + +#[derive(Serialize, Deserialize)] +pub struct JRPCError { + pub code: i64, + pub message: String, + pub data: Value, +} + +#[derive(Serialize, Deserialize)] +pub struct JRPCResult { + pub jsonrpc: String, + pub id: String, + pub result: Value, + pub error: Option, +} + +// ****************************************************************************** +// * SERVER +// ****************************************************************************** + +pub trait JRPCServiceHandler: Send { + fn get_name(&self) -> String; + fn on_message(&self, msg: JRPCRequest, function: String, ctx: &C) -> Result<(bool, Value)>; +} + +type Shared = Arc>; +type SharedHM = Shared>; +type ServiceSharedHM = SharedHM>>; + +type SharedThreadPool = Shared; + +pub struct JRPCServer { + services: ServiceSharedHM, + pool: SharedThreadPool, +} + +impl JRPCServer { + pub fn new() -> Self { + return Self { + services: Arc::new(Mutex::new(HashMap::new())), + pool: Arc::new(Mutex::new(ThreadPool::new(32))), + }; + } + + pub fn add_service(&mut self, service: Box>) { + let mut services = self.services.lock().unwrap(); + services.insert(service.get_name(), service); + } + + pub fn start_session( + &mut self, + read_ch: Receiver, + write_ch: Sender, + context: CTX, + ) { + let services = self.services.clone(); + let p = self.pool.lock().unwrap(); + let pool = self.pool.clone(); + p.execute(move || { + JRPCSession::start(read_ch, write_ch, context, services, pool); + }); + } +} + +pub struct JRPCSession { + _ctx: PhantomData, +} + +unsafe impl Sync for JRPCSession {} + +impl JRPCSession { + fn start( + read_ch: Receiver, + write_ch: Sender, + context: CTX, + services: ServiceSharedHM, + pool: SharedThreadPool, + ) { + loop { + let pkg = read_ch.recv(); + let data = match pkg { + Err(_) => return, + Ok(res) => res, + }; + if data.len() == 0 { + //TODO: This can be done better + return; + } + let ctx = context.clone(); + let svs = services.clone(); + let wc = write_ch.clone(); + pool.lock().unwrap().execute(move || { + JRPCSession::handle_packet(data, wc, ctx, svs); + }) + } + } + + fn handle_packet( + data: String, + write_ch: Sender, + context: CTX, + services: ServiceSharedHM, + ) { + let req: Result = + serde_json::from_str(data.as_str()).map_err(|err| Box::from(err)); + + let req = match req { + Err(_) => { + return; + } + Ok(parsed) => parsed, + }; + + let req_id = req.id.clone(); + + let mut parts: Vec = req.method.splitn(2, '.').map(|e| e.to_owned()).collect(); + if parts.len() != 2 { + return Self::send_err_res(req_id, write_ch, Box::from("Error".to_owned())); + } + + let service = parts.remove(0); + let function = parts.remove(0); + + let svs = services.lock().unwrap(); + let srv = svs.get(&service); + + if let Some(srv) = srv { + match srv.on_message(req, function, &context) { + Ok((is_send, value)) => { + if is_send { + if let Some(id) = req_id { + let r = JRPCResult { + jsonrpc: "2.0".to_owned(), + id, + result: value, + error: None, + }; + let s = serde_json::to_string(&r); + if s.is_ok() { + write_ch + .send(s.unwrap()) + .expect("Sending data into channel failed!"); + } + } + } + } + Err(err) => return Self::send_err_res(req_id, write_ch, err), + } + } + } + + fn send_err_res(id: Option, write_ch: Sender, err: Box) { + if let Some(id) = id { + let error = JRPCError { + code: 0, //TODO: Make this better? + message: err.to_string(), + data: Value::Null, + }; + + let r = JRPCResult { + jsonrpc: "2.0".to_owned(), + id: id.clone(), + result: Value::Null, + error: Option::from(error), + }; + + let s = serde_json::to_string(&r); + + if s.is_ok() { + write_ch + .send(s.unwrap()) + .expect("Sending data into channel failed!"); + } + } + + return (); + } +} + +// ****************************************************************************** +// * CLIENT +// ****************************************************************************** + +#[derive(Clone)] +pub struct JRPCClient { + write_ch: Sender, + requests: SharedHM>>, +} + +unsafe impl Send for JRPCClient {} //TODO: Is this a problem + +impl JRPCClient { + pub fn new(write_ch: Sender, read_ch: Receiver) -> Self { + let n = Self { + write_ch, + requests: Arc::new(Mutex::new(HashMap::new())), + }; + + n.start(read_ch); + return n; + } + + pub fn start(&self, read_ch: Receiver) { + let s = self.clone(); + std::thread::spawn(move || { + s.start_reader(read_ch); + }); + } + + fn start_reader(&self, read_ch: Receiver) { + loop { + let data = read_ch.recv().expect("Error receiving packet!"); + let response: JRPCResult = + serde_json::from_str(data.as_str()).expect("Error decoding response!"); + let id = response.id; + + let reqs = self.requests.lock().expect("Error locking requests map!"); + let req = reqs.get(&id); + if let Some(req) = req { + let res = if let Some(err) = response.error { + Err(Box::from(err.message)) + } else { + Ok(response.result) + }; + + req.send(res).expect("Error sending reponse!"); + } + } + } + + pub fn send_request(&self, mut req: JRPCRequest) -> Result { + let mut reqs = self.requests.lock().expect("Error locking requests map!"); + let id = nanoid!(); + req.id = Some(id.clone()); + + let (tx, rx) = std::sync::mpsc::channel(); + + reqs.insert(id, tx); + self + .write_ch + .send(serde_json::to_string(&req).expect("Error converting Request to JSON!")) + .expect("Error Sending to Channel!"); + return rx.recv().expect("Error getting response!"); + } + + pub fn send_notification(&self, mut req: JRPCRequest) { + req.id = None; + + self + .write_ch + .send(serde_json::to_string(&req).expect("Error converting Request to JSON!")) + .expect("Error Sending to Channel!"); + } +}