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/package.json b/package.json index 2bf5aa8..20a9fbb 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,9 @@ "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", 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..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/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"