diff --git a/Cargo.lock b/Cargo.lock index 9c6def6..c976db9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -561,7 +561,7 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jrpc-cli" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 16c8b63..65fc17d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "jrpc-cli" -version = "0.1.1" +version = "0.1.2" [workspace] resolver = "2" @@ -12,6 +12,7 @@ anyhow = "1" clap = { version = "4", features = ["derive"] } libjrpc = { path = "libjrpc" } log = "0.4" -simple_logger = { version = "5.0.0", features = ["threads"] } +simple_logger = { version = "5", features = ["threads"] } + [dev-dependencies] walkdir = "2" diff --git a/libjrpc/Cargo.toml b/libjrpc/Cargo.toml index f13714c..a1ec0b2 100644 --- a/libjrpc/Cargo.toml +++ b/libjrpc/Cargo.toml @@ -8,9 +8,9 @@ default = ["http"] http = ["dep:reqwest", "dep:url"] [dependencies] -anyhow = "1.0.89" -lazy_static = "1.5.0" -log = "0.4.22" -regex = "1.10.6" -reqwest = { version = "0.12.7", optional = true, features = ["blocking"] } -url = { version = "2.5.2", optional = true } +anyhow = "1" +lazy_static = "1" +log = "0.4" +regex = "1" +reqwest = { version = "0.12", optional = true, features = ["blocking"] } +url = { version = "2", optional = true } diff --git a/libjrpc/src/targets/csharp.rs b/libjrpc/src/targets/csharp.rs new file mode 100644 index 0000000..29c4d59 --- /dev/null +++ b/libjrpc/src/targets/csharp.rs @@ -0,0 +1,404 @@ +use anyhow::Result; +use log::warn; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::format; + +use crate::compile::{Compile, CompileContext, FileGenerator}; +use crate::ir::{BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition}; +use crate::shared::Keywords; +use crate::IR; + +pub struct CSharpCompiler { + namespace: String, +} + +static CSHARP_KEYWORDS: [&'static str; 5] = ["event", "internal", "public", "private", "static"]; + +impl CSharpCompiler { + fn type_to_csharp(typ: &BaseType) -> String { + match typ { + BaseType::String => "string".to_string(), + BaseType::Int => "long".to_string(), + BaseType::Float => "double".to_string(), + BaseType::Bool => "bool".to_string(), + BaseType::Bytes => todo!("Bytes are not implemented for C#"), + BaseType::Void => "void".to_string(), + BaseType::Custom(name) => name.clone(), + } + } + + fn type_to_csharp_ext(typ: &Type) -> String { + let mut result = Self::type_to_csharp(&typ.0); + + let (optional, array, map) = typ.1.get_flags(); + + if array { + result = format!("IList<{}>", result); + } + if let Some(map) = &map { + result = format!("Dictionary<{}, {}>", Self::type_to_csharp(&map), result); + } + if optional { + result = format!("{}?", result); + } + result + } + + fn fix_keyword_name(name: &str) -> String { + if CSHARP_KEYWORDS.contains(&name) { + format!("{}_", name) + } else { + name.to_string() + } + } + + fn generate_service_server( + &mut self, + ctx: &mut CompileContext, + definition: &ServiceDefinition, + ) -> anyhow::Result<()> { + let mut f = FileGenerator::new(); + + f.a0("using System;"); + f.a0("using System.Text.Json;"); + f.a0("using System.Text.Json.Serialization;"); + f.a0("using System.Text.Json.Nodes;"); + f.a0("using System.Threading.Tasks;"); + f.a0("using System.Collections.Generic;"); + + f.a0(""); + f.a0(format!("namespace {};", self.namespace)); + f.a0(""); + + f.a0(format!( + "public abstract class {}Server : JRpcService {{", + definition.name + )); + f.a1("public override string Name {"); + f.a2("get {"); + f.a3(format!("return \"{}\";", definition.name)); + f.a2("}"); + f.a1("}"); + + f.a1(format!("public {}Server() {{", definition.name)); + for method in &definition.methods { + f.a2(format!("this.RegisterFunction(\"{}\");", method.name)); + } + f.a1("}"); + f.a0(""); + + for fnc in &definition.methods { + let mut args = fnc + .inputs + .iter() + .map(|inp| { + format!( + "{} {}", + Self::type_to_csharp_ext(&inp.typ), + Self::fix_keyword_name(&inp.name) + ) + }) + .collect::>(); + args.push("TContext ctx".to_string()); + let args = args.join(","); + + if let Some(output) = &fnc.output { + if output.typ.0 == BaseType::Void { + f.a1(format!("public abstract Task {}({});", fnc.name, args)); + } else { + f.a1(format!( + "public abstract Task<{}> {}({});", + Self::type_to_csharp_ext(&output.typ), + fnc.name, + args + )); + } + } else { + f.a1(format!("public abstract void {}({});", fnc.name, args)); + } + } + + f.a0(""); + f.a1("public async override Task HandleRequest(string func, JsonNode param, TContext context) {"); + f.a2("switch(func) {"); + for fnc in &definition.methods { + f.a3(format!("case \"{}\": {{", fnc.name)); + f.a4("if(param is JsonObject) {"); + let args_array = fnc + .inputs + .iter() + .map(|inp| format!("param[\"{}\"]", inp.name)) + .collect::>(); + f.a5(format!( + "var ja = new JsonArray({});", + args_array.join(", ") + )); + f.a5("param = ja;"); + f.a4("}"); + + let pref = if let Some(output) = &fnc.output { + if output.typ.0 == BaseType::Void { + "await" + } else { + "var result = await" + } + } else { + "" + }; + + let mut args_array = fnc + .inputs + .iter() + .map(|inp| { + format!( + "param[\"{}\"]!.Deserialize<{}>()", + inp.name, + Self::type_to_csharp_ext(&inp.typ) + ) + }) + .collect::>(); + args_array.push("context".to_string()); + let args_array = args_array.join(", "); + f.a4(format!("{} this.{}({});", pref, fnc.name, args_array)); + if let Some(output) = &fnc.output { + if output.typ.0 == BaseType::Void { + f.a4("return null;"); + } else { + f.a4("return JsonSerializer.SerializeToNode(result);"); + } + } else { + f.a4("return null;"); + }; + f.a3("}"); + } + f.a3("default: {"); + f.a4("throw new Exception(\"Invalid Method!\");"); + f.a3("}"); + f.a2("}"); + f.a1("}"); + + f.a0("}"); + + ctx.write_file(&format!("{}Server.cs", &definition.name), &f.into_content())?; + + Ok(()) + } + + fn generate_service_client( + &mut self, + ctx: &mut CompileContext, + definition: &ServiceDefinition, + ) -> anyhow::Result<()> { + let mut f = FileGenerator::new(); + + f.a0("using System;"); + f.a0("using System.Text.Json;"); + f.a0("using System.Text.Json.Serialization;"); + f.a0("using System.Text.Json.Nodes;"); + f.a0("using System.Threading.Tasks;"); + f.a0("using System.Collections.Generic;"); + + f.a0(""); + f.a0(format!("namespace {};", self.namespace)); + f.a0(""); + + f.a0(format!("public class {}Client {{", definition.name)); + f.a1("private JRpcClient Client;"); + f.a1(format!( + "public {}Client(JRpcClient client) {{", + definition.name + )); + f.a2("this.Client = client;"); + f.a1("}"); + f.a0(""); + for fnc in &definition.methods { + let args = fnc + .inputs + .iter() + .map(|inp| { + format!( + "{} {}", + Self::type_to_csharp_ext(&inp.typ), + Self::fix_keyword_name(&inp.name) + ) + }) + .collect::>() + .join(", "); + + let args_code = format!( + "var param = new JsonArray({});", + fnc.inputs + .iter() + .map(|inp| { + format!( + "JsonSerializer.SerializeToNode({})", + Self::fix_keyword_name(&inp.name) + ) + }) + .collect::>() + .join(", ") + ); + + if let Some(output) = &fnc.output { + if output.typ.0 == BaseType::Void { + f.a1(format!("public async Task {}({}) {{", fnc.name, args)); + f.a2(args_code); + f.a2(format!( + "await Client.SendRequestRaw(\"{}.{}\", param);", + definition.name, fnc.name, + )); + f.a1("}"); + } else { + f.a1(format!( + "public async Task<{}> {}({}) {{", + Self::type_to_csharp_ext(&output.typ), + fnc.name, + args + )); + f.a2(args_code); + f.a2(format!( + "return await Client.SendRequest<{}>(\"{}.{}\", param);", + Self::type_to_csharp_ext(&output.typ), + definition.name, + fnc.name, + )); + f.a1("}"); + } + } else { + f.a1(format!("public void {}({}) {{", fnc.name, args)); + f.a2(args_code); + f.a2(format!( + "Client.SendNotification(\"{}.{}\", param);", + definition.name, fnc.name, + )); + f.a1("}"); + } + } + + f.a0("}"); + + ctx.write_file(&format!("{}Client.cs", &definition.name), &f.into_content())?; + + Ok(()) + } +} + +impl Compile for CSharpCompiler { + fn new(options: &BTreeMap) -> anyhow::Result { + let namespace = if let Some(namespace) = options.get("csharp_namespace") { + namespace.to_string() + } else { + "JRPC".to_string() + }; + + if let Some(allow_bytes) = options.get("allow_bytes") { + if allow_bytes == "true" { + anyhow::bail!("allow_bytes option is not supported for csharp compiler"); + } + } + + Ok(CSharpCompiler { namespace }) + } + + fn name(&self) -> String { + "csharp".to_string() + } + + fn start(&mut self, ctx: &mut CompileContext) -> anyhow::Result<()> { + let fix_ns = |input: &str| input.replace("__NAMESPACE__", &self.namespace); + + ctx.write_file( + &format!("{}.csproj", self.namespace), + &fix_ns(include_str!("../../templates/CSharp/CSharp.csproj")), + )?; + + ctx.write_file( + "JRpcClient.cs", + &fix_ns(include_str!("../../templates/CSharp/JRpcClient.cs")), + )?; + + ctx.write_file( + "JRpcServer.cs", + &fix_ns(include_str!("../../templates/CSharp/JRpcServer.cs")), + )?; + + ctx.write_file( + "JRpcTransport.cs", + &fix_ns(include_str!("../../templates/CSharp/JRpcTransport.cs")), + )?; + + Ok(()) + } + + fn generate_type( + &mut self, + ctx: &mut CompileContext, + definition: &TypeDefinition, + ) -> anyhow::Result<()> { + let mut f = FileGenerator::new(); + + f.a0("using System.Text.Json;"); + f.a0("using System.Text.Json.Serialization;"); + f.a0("using System.Collections.Generic;"); + f.a0(""); + f.a0(format!("namespace {};", self.namespace)); + f.a0(""); + f.a0(format!("public class {} {{", definition.name)); + + for field in &definition.fields { + f.a1(format!("[JsonPropertyName(\"{}\")]", field.name)); + + f.a1(format!( + "public {} {} {{ get; set; }}", + Self::type_to_csharp_ext(&field.typ), + Self::fix_keyword_name(&field.name) + )); + } + + f.a0("}"); + + ctx.write_file(&format!("{}.cs", &definition.name), &f.into_content())?; + + Ok(()) + } + + fn generate_enum( + &mut self, + ctx: &mut CompileContext, + definition: &EnumDefinition, + ) -> anyhow::Result<()> { + let mut f = FileGenerator::new(); + + f.a0("using System.Text.Json;"); + f.a0("using System.Text.Json.Serialization;"); + f.a0(""); + f.a0(format!("namespace {};", self.namespace)); + f.a0(""); + + f.a0(format!("public enum {} {{", definition.name)); + + for variant in &definition.values { + f.a1(format!("{} = {},", variant.name, variant.value)); + } + + f.a0("}"); + + ctx.write_file(&format!("{}.cs", &definition.name), &f.into_content())?; + + Ok(()) + } + + fn generate_service( + &mut self, + ctx: &mut CompileContext, + definition: &ServiceDefinition, + ) -> anyhow::Result<()> { + self.generate_service_client(ctx, definition)?; + self.generate_service_server(ctx, definition)?; + Ok(()) + } + + fn finalize(&mut self, ctx: &mut CompileContext, ir: &IR) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/libjrpc/src/targets/mod.rs b/libjrpc/src/targets/mod.rs index 111ac16..720585d 100644 --- a/libjrpc/src/targets/mod.rs +++ b/libjrpc/src/targets/mod.rs @@ -11,6 +11,7 @@ use crate::{ IR, }; +pub mod csharp; pub mod rust; pub mod typescript; diff --git a/src/main.rs b/src/main.rs index 0c5b1b6..8a7b93e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use libjrpc::{ targets::{ + csharp::CSharpCompiler, rust::RustCompiler, typescript::{Node, TypeScriptCompiler}, }, @@ -67,6 +68,7 @@ pub fn main() -> Result<()> { "ts-esm" => { libjrpc::targets::compile::>(ir, output_dir)? } + "csharp" => libjrpc::targets::compile::(ir, output_dir)?, _ => { println!("Unsupported target: {}", output_target); } @@ -83,6 +85,7 @@ pub fn main() -> Result<()> { println!("rust"); println!("ts-node"); println!("ts-esm"); + println!("csharp") } }