diff --git a/crates/libjrpc/src/compile.rs b/crates/libjrpc/src/compile.rs index edb121f..0ac7ebc 100644 --- a/crates/libjrpc/src/compile.rs +++ b/crates/libjrpc/src/compile.rs @@ -2,24 +2,37 @@ use std::{collections::HashMap, path::PathBuf}; use anyhow::{Context, Result}; -use crate::ir::{EnumDefinition, ServiceDefinition, TypeDefinition}; +use crate::{ + ir::{EnumDefinition, ServiceDefinition, TypeDefinition}, + IR, +}; pub trait Compile { + fn new(options: &HashMap) -> Result + where + Self: Sized; + fn name(&self) -> String; - fn start(&mut self, ctx: &mut CompileContext, options: HashMap) -> Result<()>; + fn start(&mut self, ctx: &mut CompileContext) -> Result<()>; - fn generate_type(&mut self, ctx: &mut CompileContext, definition: TypeDefinition) - -> Result<()>; - fn generate_enum(&mut self, ctx: &mut CompileContext, definition: EnumDefinition) - -> Result<()>; + fn generate_type( + &mut self, + ctx: &mut CompileContext, + definition: &TypeDefinition, + ) -> Result<()>; + fn generate_enum( + &mut self, + ctx: &mut CompileContext, + definition: &EnumDefinition, + ) -> Result<()>; fn generate_service( &mut self, ctx: &mut CompileContext, - definition: ServiceDefinition, + definition: &ServiceDefinition, ) -> Result<()>; - fn finalize(&mut self, ctx: &mut CompileContext) -> Result<()>; + fn finalize(&mut self, ctx: &mut CompileContext, ir: &IR) -> Result<()>; } pub struct CompileContext { @@ -42,34 +55,57 @@ impl CompileContext { } pub struct FileGenerator { - content: String, + content: Vec, } impl FileGenerator { pub fn new() -> Self { FileGenerator { - content: String::new(), + content: Vec::new(), } } - pub fn a(&mut self, indent: u32, content: T) { - for _ in 0..indent { - self.content.push_str(" "); - } - self.content.push_str(&content.to_string()); - self.content.push_str("\n"); + pub fn a(&mut self, indent: usize, content: T) { + let line = " ".repeat(indent) + &content.to_string(); + self.content.push(line); + } + + pub fn a0(&mut self, content: T) { + self.a(0, content); + } + + pub fn a1(&mut self, content: T) { + self.a(1, content); + } + + pub fn a2(&mut self, content: T) { + self.a(2, content); + } + + pub fn a3(&mut self, content: T) { + self.a(3, content); + } + + pub fn a4(&mut self, content: T) { + self.a(4, content); + } + + pub fn a5(&mut self, content: T) { + self.a(5, content); + } + pub fn a6(&mut self, content: T) { + self.a(6, content); } pub fn add_line(&mut self, line: &str) { - self.content.push_str(line); - self.content.push_str("\n"); + self.content.push(line.to_string()); } - pub fn get_content(&mut self) -> String { - self.content.clone() + pub fn get_content(&self) -> String { + self.content.join("\n") } pub fn into_content(self) -> String { - self.content + self.get_content() } } diff --git a/crates/libjrpc/src/ir.rs b/crates/libjrpc/src/ir.rs index 3afc2d8..9680541 100644 --- a/crates/libjrpc/src/ir.rs +++ b/crates/libjrpc/src/ir.rs @@ -11,7 +11,7 @@ use crate::parser::{ EnumStatement, Node, ParserPosition, RootNode, ServiceStatement, TypeStatement, }; -static BUILT_INS: [&str; 5] = ["int", "float", "string", "boolean", "bytes"]; +static BUILT_INS: [&str; 6] = ["int", "float", "string", "boolean", "bytes", "void"]; pub trait Definition { fn get_position(&self) -> ParserPosition; @@ -37,17 +37,53 @@ pub enum Type { String, Bool, Bytes, + Void, Custom(String), } + +impl ToString for Type { + fn to_string(&self) -> String { + match self { + Type::Int => "int".to_string(), + Type::Float => "float".to_string(), + Type::String => "string".to_string(), + Type::Bool => "bool".to_string(), + Type::Bytes => "bytes".to_string(), + Type::Void => "void".to_string(), + Type::Custom(name) => name.clone(), + } + } +} + impl Hash for Type { fn hash(&self, state: &mut H) { - match self { - Type::Int => "int".hash(state), - Type::Float => "float".hash(state), - Type::String => "string".hash(state), - Type::Bool => "bool".hash(state), - Type::Bytes => "bytes".hash(state), - Type::Custom(name) => name.hash(state), + self.to_string().hash(state); + } +} + +impl From<&String> for Type { + fn from(value: &String) -> Self { + Self::from(value.as_str()) + } +} + +impl From for Type { + fn from(value: String) -> Self { + Self::from(value.as_str()) + } +} + +impl From<&str> for Type { + fn from(s: &str) -> Self { + match s { + "int" => Type::Int, + "float" => Type::Float, + "string" => Type::String, + "bool" => Type::Bool, + "boolean" => Type::Bool, + "bytes" => Type::Bytes, + "void" => Type::Void, + _ => Type::Custom(s.to_string()), } } } @@ -137,17 +173,6 @@ pub struct MethodDecorators { pub return_description: Option, } -fn typename_to_type(name: &str) -> Type { - match name { - "int" => Type::Int, - "float" => Type::Float, - "string" => Type::String, - "boolean" => Type::Bool, - "bool" => Type::Bool, - _ => Type::Custom(name.to_string()), - } -} - fn build_type(stmt: &TypeStatement) -> Result { let mut typedef = TypeDefinition { position: stmt.position.clone(), @@ -157,7 +182,7 @@ fn build_type(stmt: &TypeStatement) -> Result { }; for field in &stmt.fields { - let typ = typename_to_type(&field.fieldtype); + let typ = Type::from(&field.fieldtype); typedef.depends.insert(typ.clone()); if let Some(maptype) = &field.map { @@ -171,7 +196,7 @@ fn build_type(stmt: &TypeStatement) -> Result { typ: typ.clone(), array: field.array, optional: field.optional, - map: field.map.as_ref().map(|s| typename_to_type(s)), + map: field.map.as_ref().map(|s| Type::from(s)), }); } @@ -222,8 +247,8 @@ fn build_service(stmt: &ServiceStatement) -> Result { name: method.name.clone(), inputs: Vec::new(), output: method.return_type.as_ref().map(|rt| { - let typ = typename_to_type(&rt.fieldtype); - if typ != Type::Custom("void".to_string()) { + let typ = Type::from(&rt.fieldtype); + if typ != Type::Void { servdef.depends.insert(typ.clone()); } MethodOutput { @@ -240,7 +265,7 @@ fn build_service(stmt: &ServiceStatement) -> Result { let mut optional_starts = false; for inp in &method.inputs { - let typ = typename_to_type(&inp.fieldtype); + let typ = Type::from(&inp.fieldtype); servdef.depends.insert(typ.clone()); if optional_starts && !inp.optional { diff --git a/crates/libjrpc/src/lib.rs b/crates/libjrpc/src/lib.rs index d3f7464..0457980 100644 --- a/crates/libjrpc/src/lib.rs +++ b/crates/libjrpc/src/lib.rs @@ -13,6 +13,11 @@ pub use tokenizer::{tokenize, Token, TokenError, TokenPosition, TokenType}; #[cfg(test)] mod test { + use crate::{ + compile::{Compile, CompileContext}, + targets::{self, rust::RustCompiler}, + }; + #[cfg(test)] #[ctor::ctor] fn init() { @@ -27,4 +32,12 @@ mod test { println!("{:?}", ir); } + + #[test] + pub fn generate_rust() { + let mut fp = crate::process::FileProcessor::new(); + let ir = fp.start_compile("./test.jrpc").unwrap(); + + targets::compile::(ir, "./output/rust").unwrap(); + } } diff --git a/crates/libjrpc/src/targets/mod.rs b/crates/libjrpc/src/targets/mod.rs index 3065205..b76f306 100644 --- a/crates/libjrpc/src/targets/mod.rs +++ b/crates/libjrpc/src/targets/mod.rs @@ -1 +1,28 @@ -mod rust; +use anyhow::Result; + +use crate::{ + compile::{Compile, CompileContext}, + IR, +}; + +pub mod rust; + +pub fn compile(ir: IR, output: &str) -> Result<()> { + let mut ctx = CompileContext::new(output); + let mut compiler = T::new(&ir.options)?; + compiler.start(&mut ctx)?; + + for step in ir.steps.iter() { + match step { + crate::ir::Step::Type(definition) => compiler.generate_type(&mut ctx, &definition)?, + crate::ir::Step::Enum(definition) => compiler.generate_enum(&mut ctx, &definition)?, + crate::ir::Step::Service(definition) => { + compiler.generate_service(&mut ctx, &definition)? + } + } + } + + compiler.finalize(&mut ctx, &ir)?; + + Ok(()) +} diff --git a/crates/libjrpc/src/targets/rust.rs b/crates/libjrpc/src/targets/rust.rs index ce7e982..7549109 100644 --- a/crates/libjrpc/src/targets/rust.rs +++ b/crates/libjrpc/src/targets/rust.rs @@ -3,25 +3,40 @@ use log::warn; use std::collections::{HashMap, HashSet}; use crate::compile::{Compile, CompileContext, FileGenerator}; -use crate::ir::{EnumDefinition, ServiceDefinition, Type, TypeDefinition}; +use crate::ir::{EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition}; use crate::shared::Keywords; +use crate::IR; pub struct RustCompiler { crate_name: String, } +static RUST_KEYWORDS: [&'static str; 6] = ["type", "return", "static", "pub", "enum", "self"]; + impl RustCompiler { - fn type_to_rust(typ: Type) -> String { + fn type_to_rust(typ: &Type) -> String { match typ { Type::String => "String".to_string(), Type::Int => "i64".to_string(), Type::Float => "f64".to_string(), Type::Bool => "bool".to_string(), Type::Bytes => "Vec".to_string(), - Type::Custom(name) => name, + Type::Void => "()".to_string(), + Type::Custom(name) => name.clone(), } } + fn type_to_rust_ext(typ: &Type, optional: bool, array: bool) -> String { + let mut result = Self::type_to_rust(typ); + if optional { + result = format!("Option<{}>", result); + } + if array { + result = format!("Vec<{}>", result); + } + result + } + fn add_dependencies( &mut self, file: &mut FileGenerator, @@ -30,18 +45,18 @@ impl RustCompiler { for dep in depends { match dep { Type::Custom(name) => { - file.a(0, &format!("use crate::{};", name)); + file.a0(&format!("use crate::{};", name)); } _ => {} } } - file.a(0, ""); - file.a(0, ""); + file.a0(""); + file.a0(""); Ok(()) } fn fix_keyword_name(name: &str) -> String { - if Keywords::is_keyword(name) { + if RUST_KEYWORDS.contains(&name) { format!("{}_", name) } else { name.to_string() @@ -69,23 +84,333 @@ impl RustCompiler { } result } + + fn generate_service_lib(ctx: &CompileContext, ir: &IR) -> Result<()> { + let mut f = FileGenerator::new(); + let mut fc = FileGenerator::new(); + let mut fs = FileGenerator::new(); + + f.a0("pub mod base_lib;"); + f.a0("pub use base_lib::{JRPCServer, JRPCClient, Result};"); + + for step in ir.steps.iter() { + match step { + Step::Type(def) => { + f.a0(format!("mod {};", Self::to_snake(&def.name))); + f.a( + 0, + format!("pub use {}::{};", Self::to_snake(&def.name), def.name), + ); + } + Step::Enum(def) => { + f.a0(format!("mod {};", Self::to_snake(&def.name))); + f.a( + 0, + format!("pub use {}::{};", Self::to_snake(&def.name), def.name), + ); + } + Step::Service(def) => { + fs.a0(format!("mod {};", Self::to_snake(&def.name))); + fs.a0(format!( + "pub use {}::{{ {}, {}Handler }};", + Self::to_snake(&def.name), + def.name, + def.name + )); + + fc.a0(format!("mod {};", Self::to_snake(&def.name))); + fc.a0(format!( + "pub use {}::{};", + Self::to_snake(&def.name), + def.name, + )); + } + } + } + + f.a0("pub mod server;"); + f.a0("pub mod client;"); + + ctx.write_file("src/lib.rs", f.get_content())?; + ctx.write_file("src/server/mod.rs", fs.get_content())?; + ctx.write_file("src/client/mod.rs", fc.get_content())?; + + Ok(()) + } + + fn generate_service_server( + &mut self, + ctx: &mut CompileContext, + definition: &ServiceDefinition, + ) -> anyhow::Result<()> { + let mut f = FileGenerator::new(); + + self.add_dependencies(&mut f, &definition.depends)?; + + f.a0("use crate::base_lib::{JRPCServerService, JRPCRequest, Result};"); + f.a0("use serde_json::Value;"); + f.a0("use std::sync::Arc;"); + f.a0("use async_trait::async_trait;"); + + f.a0("#[async_trait]"); + f.a0(format!("pub trait {} {{", definition.name)); + for method in definition.methods.iter() { + let params = method + .inputs + .iter() + .map(|arg| { + format!( + "{}: {}", + Self::fix_keyword_name(&arg.name), + Self::type_to_rust_ext(&arg.typ, arg.optional, arg.array) + ) + }) + .collect::>() + .join(", "); + + let ret = method.output.as_ref().map_or_else( + || "()".to_owned(), + |r| Self::type_to_rust_ext(&r.typ, false, r.array), + ); + + f.a1("#[allow(non_snake_case)]"); + f.a( + 1, + format!( + "async fn {}(&self, {}) -> Result<{}>;", + Self::fix_keyword_name(&method.name), + params, + ret + ), + ); + } + f.a0("}"); + + f.a0(""); + f.a0(format!("pub struct {}Handler {{", definition.name)); + f.a1(format!( + "implementation: Box,", + definition.name + )); + f.a0("}"); + f.a0(""); + + f.a0(format!("impl {}Handler {{", definition.name)); + f.a1(format!( + "pub fn new(implementation: Box) -> Arc {{", + definition.name, + )); + f.a2("Arc::from(Self { implementation })"); + f.a1("}"); + f.a0("}"); + + f.a0(""); + + f.a0("#[async_trait]"); + f.a0(format!( + "impl JRPCServerService for {}Handler {{", + definition.name + )); + f.a1(format!( + "fn get_id(&self) -> String {{ \"{}\".to_owned() }} ", + definition.name + )); + + f.a1(""); + + f.a1("#[allow(non_snake_case)]"); + f.a1( + "async fn handle(&self, msg: &JRPCRequest, function: &str) -> Result<(bool, Value)> {", + ); + f.a2("match function {"); + + // TODO: Implement optional arguments! + + for method in &definition.methods { + f.a3(format!( + "\"{}\" => {{", + Self::fix_keyword_name(&method.name) + )); + if method.inputs.len() < 1 { + f.a5(format!( + "let res = self.implementation.{}().await?;", + method.name + )); + f.a5("Ok((true, serde_json::to_value(res)?))"); + } else { + f.a4("if msg.params.is_array() {"); + if method.inputs.len() > 0 { + f.a5( + "let arr = msg.params.as_array().unwrap(); //TODO: Check if this can fail.", + ); + } + f.a5(format!("let res = self.implementation.{}(", method.name)); + for (i, arg) in method.inputs.iter().enumerate() { + f.a6(format!("serde_json::from_value(arr[{}].clone())", i)); + f.a( + 7, + format!( + ".map_err(|_| \"Parameter for field '{}' should be of type '{}'!\")?{}", + arg.name, + arg.typ.to_string(), + if i < method.inputs.len() - 1 { "," } else { "" } + ), + ); + } + f.a5(").await?;"); + + if let Some(_output) = &method.output { + f.a5("Ok((true, serde_json::to_value(res)?))"); + } else { + f.a5("_ = res;"); + f.a5("Ok((true, Value::Null))"); + } + + f.a4("} else if msg.params.is_object() {"); + f.a5("let obj = msg.params.as_object().unwrap(); //TODO: Check if this can fail."); + f.a5(format!("let res = self.implementation.{}(", method.name)); + for (i, arg) in method.inputs.iter().enumerate() { + f.a6(format!( + "serde_json::from_value(obj.get(\"{}\").ok_or(\"Parameter of field '{}' missing!\")?.clone())", + arg.name, + arg.name + )); + f.a( + 7, + format!( + ".map_err(|_| \"Parameter for field {} should be of type '{}'!\")?{}", + arg.name, + arg.typ.to_string(), + if i < method.inputs.len() - 1 { "," } else { "" } + ), + ); + } + f.a5(").await?;"); + if let Some(_output) = &method.output { + f.a5("Ok((true, serde_json::to_value(res)?))"); + } else { + f.a5("Ok((false, Value::Null))"); + } + f.a4("} else {"); + f.a5("Err(Box::from(\"Invalid parameters??\".to_owned()))"); + f.a4("}"); + } + f.a3("}"); + } + f.a3("_ => { Err(Box::from(format!(\"Invalid function {}\", function).to_owned())) },"); + + f.a2("}"); + f.a1("}"); + f.a0("}"); + + ctx.write_file( + &format!("src/server/{}.rs", Self::to_snake(&definition.name)), + f.into_content(), + )?; + + Ok(()) + } + + fn generate_service_client( + &mut self, + ctx: &mut CompileContext, + definition: &ServiceDefinition, + ) -> anyhow::Result<()> { + let mut f = FileGenerator::new(); + + self.add_dependencies(&mut f, &definition.depends)?; + + f.a0("use crate::base_lib::{JRPCClient, JRPCRequest, Result};"); + f.a0("use serde_json::json;"); + f.a0(""); + f.a0(format!("pub struct {} {{", definition.name)); + f.a1("client: JRPCClient,"); + f.a0("}"); + f.a0(""); + + f.a0(format!("impl {} {{", definition.name)); + f.a1("pub fn new(client: JRPCClient) -> Self {"); + f.a2(format!("Self {{ client }}")); + f.a1("}"); + + f.a0(""); + + for method in &definition.methods { + let params = method + .inputs + .iter() + .map(|arg| { + format!( + "{}: {}", + Self::fix_keyword_name(&arg.name), + Self::type_to_rust_ext(&arg.typ, arg.optional, arg.array) + ) + }) + .collect::>() + .join(", "); + let ret = method.output.as_ref().map_or("()".to_string(), |output| { + Self::type_to_rust_ext(&output.typ, false, output.array) + }); + + f.a1("#[allow(non_snake_case)]"); + f.a1(format!( + "pub async fn {}(&self, {}) -> Result<{}> {{", + method.name, params, ret + )); + + f.a2("let l_req = JRPCRequest {"); + f.a3("jsonrpc: \"2.0\".to_owned(),"); + f.a3("id: None, // 'id' will be set by the send_request function"); + f.a3(format!( + "method: \"{}.{}\".to_owned(),", + definition.name, method.name + )); + f.a3(format!( + "params: json!([{}])", + method + .inputs + .iter() + .map(|e| Self::fix_keyword_name(&e.name)) + .collect::>() + .join(", ") + )); + f.a2("};"); + + if let Some(output) = &method.output { + f.a2("let l_res = self.client.send_request(l_req).await;"); + f.a2("match l_res {"); + f.a3("Err(e) => Err(e),"); + if output.typ == Type::Void { + f.a3("Ok(_) => Ok(())"); + } else { + f.a3("Ok(o) => serde_json::from_value(o).map_err(|e| Box::from(e))"); + } + f.a2("}"); + } else { + f.a2("self.client.send_notification(l_req).await;"); + f.a2("Ok(())"); + } + f.a1("}"); + } + + f.a0("}"); + + ctx.write_file( + &format!("src/client/{}.rs", Self::to_snake(&definition.name)), + f.into_content(), + )?; + + Ok(()) + } } impl Compile for RustCompiler { - fn name(&self) -> String { - "rust".to_string() - } - - fn start( - &mut self, - ctx: &mut CompileContext, - options: HashMap, - ) -> anyhow::Result<()> { - if let Some(crate_name) = options.get("crate") { - self.crate_name = crate_name.to_string(); + fn new(options: &HashMap) -> anyhow::Result { + let crate_name = if let Some(crate_name) = options.get("rust_crate") { + crate_name.to_string() } else { anyhow::bail!("crate option is required for rust compiler"); - } + }; if let Some(allow_bytes) = options.get("allow_bytes") { if allow_bytes == "true" { @@ -93,6 +418,14 @@ impl Compile for RustCompiler { } } + Ok(RustCompiler { crate_name }) + } + + fn name(&self) -> String { + "rust".to_string() + } + + fn start(&mut self, ctx: &mut CompileContext) -> anyhow::Result<()> { ctx.write_file( "Cargo.toml", include_str!("../../templates/Rust/Cargo.toml") @@ -105,28 +438,28 @@ impl Compile for RustCompiler { include_str!("../../templates/Rust/src/lib.rs").to_owned(), )?; - todo!() + Ok(()) } fn generate_type( &mut self, ctx: &mut CompileContext, - definition: TypeDefinition, + definition: &TypeDefinition, ) -> anyhow::Result<()> { let mut f = FileGenerator::new(); if definition.fields.iter().any(|e| e.map.is_some()) { - f.a(0, "use std::collections::hash_map::HashMap;") + f.a0("use std::collections::hash_map::HashMap;") } - f.a(0, "use serde::{Deserialize, Serialize};"); + f.a0("use serde::{Deserialize, Serialize};"); self.add_dependencies(&mut f, &definition.depends)?; - f.a(0, "#[derive(Clone, Debug, Serialize, Deserialize)]"); - f.a(0, format!("pub struct {} {{", definition.name)); - for field in definition.fields { + f.a0("#[derive(Clone, Debug, Serialize, Deserialize)]"); + f.a0(format!("pub struct {} {{", definition.name)); + for field in definition.fields.iter() { f.a(1, "#[allow(non_snake_case)]"); - let mut func = format!("pub {}: ", Self::fix_keyword_name(&field.name)); + let func = format!("pub {}:", Self::fix_keyword_name(&field.name)); if Keywords::is_keyword(&field.name) { warn!( @@ -134,7 +467,7 @@ impl Compile for RustCompiler { field.name, field.name ); - f.a(1, "#[serde(rename = \"type\")]"); + f.a(1, format!("#[serde(rename = \"{}\")]", field.name)); } let mut opts = String::new(); @@ -152,19 +485,19 @@ impl Compile for RustCompiler { "{} {}Vec<{}>{},", func, opts, - Self::type_to_rust(field.typ), + Self::type_to_rust(&field.typ), opte ), ); - } else if let Some(map) = field.map { + } else if let Some(map) = &field.map { f.a( 1, format!( - "{} {}HashMap<{},{}>{},", + "{} {}HashMap<{}, {}>{},", func, opts, Self::type_to_rust(map), - Self::type_to_rust(field.typ), + Self::type_to_rust(&field.typ), opte ), ); @@ -175,14 +508,14 @@ impl Compile for RustCompiler { "{} {}{}{},", func, opts, - Self::type_to_rust(field.typ), + Self::type_to_rust(&field.typ), opte ), ); } } - f.a(0, "}"); + f.a0("}"); ctx.write_file( &format!("src/{}.rs", Self::to_snake(&definition.name)), @@ -195,20 +528,41 @@ impl Compile for RustCompiler { fn generate_enum( &mut self, ctx: &mut CompileContext, - definition: EnumDefinition, + definition: &EnumDefinition, ) -> anyhow::Result<()> { - todo!() + let mut f = FileGenerator::new(); + f.a0("use int_enum::IntEnum;"); + // f.a0("use serde::{Deserialize, Serialize};"); + f.a0(""); + f.a0(""); + f.a0("#[repr(i64)]"); + f.a0("#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)]"); + f.a0(format!("pub enum {} {{", definition.name)); + for val in definition.values.iter() { + f.a(1, format!("{} = {},", val.name, val.value)); + } + f.a0("}"); + + ctx.write_file( + &format!("src/{}.rs", Self::to_snake(&definition.name)), + f.into_content(), + )?; + + Ok(()) } fn generate_service( &mut self, ctx: &mut CompileContext, - definition: ServiceDefinition, + definition: &ServiceDefinition, ) -> anyhow::Result<()> { - todo!() + self.generate_service_client(ctx, definition)?; + self.generate_service_server(ctx, definition)?; + Ok(()) } - fn finalize(&mut self, ctx: &mut CompileContext) -> anyhow::Result<()> { - todo!() + fn finalize(&mut self, ctx: &mut CompileContext, ir: &IR) -> anyhow::Result<()> { + Self::generate_service_lib(ctx, ir)?; + Ok(()) } } diff --git a/crates/libjrpc/templates/Rust/Cargo.toml b/crates/libjrpc/templates/Rust/Cargo.toml index 8ac3329..84d3258 100644 --- a/crates/libjrpc/templates/Rust/Cargo.toml +++ b/crates/libjrpc/templates/Rust/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -int-enum = { version ="0.5.0", features = ["serde", "convert"] } +int-enum = { version = "0.5.0", features = ["serde", "convert"] } serde = { version = "1.0.147", features = ["derive"] } serde_json = "1.0.88" nanoid = "0.4.0"