Restructure and start working on CLI

This commit is contained in:
Fabian Stamm
2025-05-26 16:43:40 +02:00
parent 883b6da7eb
commit b61518de00
38 changed files with 134 additions and 8 deletions

View File

@ -0,0 +1,28 @@
use anyhow::Result;
use crate::{
compile::{Compile, CompileContext},
IR,
};
pub mod rust;
pub fn compile<T: 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(())
}

568
libjrpc/src/targets/rust.rs Normal file
View File

@ -0,0 +1,568 @@
use anyhow::Result;
use log::warn;
use std::collections::{HashMap, HashSet};
use crate::compile::{Compile, CompileContext, FileGenerator};
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 {
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<u8>".to_string(),
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,
depends: &HashSet<Type>,
) -> Result<()> {
for dep in depends {
match dep {
Type::Custom(name) => {
file.a0(&format!("use crate::{};", name));
}
_ => {}
}
}
file.a0("");
file.a0("");
Ok(())
}
fn fix_keyword_name(name: &str) -> String {
if RUST_KEYWORDS.contains(&name) {
format!("{}_", name)
} else {
name.to_string()
}
}
fn to_snake(name: &str) -> String {
let mut result = String::new();
let mut last_upper = false;
for c in name.chars() {
if c.is_uppercase() {
if last_upper {
result.push(c.to_ascii_lowercase());
} else {
if !result.is_empty() {
result.push('_');
}
result.push(c.to_ascii_lowercase());
}
last_upper = true;
} else {
result.push(c);
last_upper = false;
}
}
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::<Vec<String>>()
.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<dyn {} + Sync + Send + 'static>,",
definition.name
));
f.a0("}");
f.a0("");
f.a0(format!("impl {}Handler {{", definition.name));
f.a1(format!(
"pub fn new(implementation: Box<dyn {} + Sync + Send + 'static>) -> Arc<Self> {{",
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::<Vec<String>>()
.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::<Vec<String>>()
.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 new(options: &HashMap<String, String>) -> anyhow::Result<Self> {
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" {
anyhow::bail!("allow_bytes option is not supported for rust compiler");
}
}
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")
.to_owned()
.replace("__name__", &self.crate_name),
)?;
ctx.write_file(
"src/base_lib.rs",
include_str!("../../templates/Rust/src/lib.rs").to_owned(),
)?;
Ok(())
}
fn generate_type(
&mut self,
ctx: &mut CompileContext,
definition: &TypeDefinition,
) -> anyhow::Result<()> {
let mut f = FileGenerator::new();
if definition.fields.iter().any(|e| e.map.is_some()) {
f.a0("use std::collections::hash_map::HashMap;")
}
f.a0("use serde::{Deserialize, Serialize};");
self.add_dependencies(&mut f, &definition.depends)?;
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 func = format!("pub {}:", Self::fix_keyword_name(&field.name));
if Keywords::is_keyword(&field.name) {
warn!(
"[RUST] Warning: Field name '{}' is not allowed in Rust. Renaming to '{}_'",
field.name, field.name
);
f.a(1, format!("#[serde(rename = \"{}\")]", field.name));
}
let mut opts = String::new();
let mut opte = String::new();
if field.optional {
opts = "Option<".to_string();
opte = ">".to_string();
}
if field.array {
f.a(
1,
format!(
"{} {}Vec<{}>{},",
func,
opts,
Self::type_to_rust(&field.typ),
opte
),
);
} else if let Some(map) = &field.map {
f.a(
1,
format!(
"{} {}HashMap<{}, {}>{},",
func,
opts,
Self::type_to_rust(map),
Self::type_to_rust(&field.typ),
opte
),
);
} else {
f.a(
1,
format!(
"{} {}{}{},",
func,
opts,
Self::type_to_rust(&field.typ),
opte
),
);
}
}
f.a0("}");
ctx.write_file(
&format!("src/{}.rs", Self::to_snake(&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("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,
) -> 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<()> {
Self::generate_service_lib(ctx, ir)?;
Ok(())
}
}