4 Commits
0.0.1 ... 0.1.1

Author SHA1 Message Date
369ccbe84e Make sure that imports are always in the same order between runs. This
makes the output more stable for putting it into a versioning system
like git
2025-07-26 13:12:34 +02:00
6cb51c4120 Cleanup 2025-05-29 20:28:15 +02:00
bfb8c076be Add Context to Rust service 2025-05-28 15:54:53 +02:00
f4b32f650c Make optional types better optional in typescript 2025-05-28 13:20:03 +02:00
9 changed files with 119 additions and 68 deletions

2
Cargo.lock generated
View File

@ -561,7 +561,7 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "jrpc-cli" name = "jrpc-cli"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",

View File

@ -1,7 +1,7 @@
[package] [package]
edition = "2021" edition = "2021"
name = "jrpc-cli" name = "jrpc-cli"
version = "0.1.0" version = "0.1.1"
[workspace] [workspace]
resolver = "2" resolver = "2"

View File

@ -1,4 +1,4 @@
use std::{collections::HashMap, path::PathBuf}; use std::{collections::BTreeMap, path::PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@ -8,7 +8,7 @@ use crate::{
}; };
pub trait Compile { pub trait Compile {
fn new(options: &HashMap<String, String>) -> Result<Self> fn new(options: &BTreeMap<String, String>) -> Result<Self>
where where
Self: Sized; Self: Sized;

View File

@ -1,5 +1,5 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{BTreeMap, BTreeSet},
error::Error, error::Error,
fmt::{Debug, Display}, fmt::{Debug, Display},
hash::{Hash, Hasher}, hash::{Hash, Hasher},
@ -20,7 +20,7 @@ pub trait Definition: Debug {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct IR { pub struct IR {
pub options: HashMap<String, String>, pub options: BTreeMap<String, String>,
pub steps: Vec<Step>, pub steps: Vec<Step>,
} }
@ -58,7 +58,12 @@ impl ToString for BaseType {
impl Hash for BaseType { impl Hash for BaseType {
fn hash<H: Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
self.to_string().hash(state); // Hash the enum variant itself
std::mem::discriminant(self).hash(state);
// If the variant has data, hash that too
if let BaseType::Custom(name) = self {
name.hash(state);
}
} }
} }
@ -178,7 +183,7 @@ impl Type {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TypeDefinition { pub struct TypeDefinition {
pub name: String, pub name: String,
pub depends: HashSet<BaseType>, pub depends: BTreeSet<BaseType>,
pub fields: Vec<Field>, pub fields: Vec<Field>,
pub position: ParserPosition, pub position: ParserPosition,
} }
@ -223,7 +228,7 @@ pub struct EnumField {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ServiceDefinition { pub struct ServiceDefinition {
pub name: String, pub name: String,
pub depends: HashSet<BaseType>, pub depends: BTreeSet<BaseType>,
pub methods: Vec<Method>, pub methods: Vec<Method>,
pub position: ParserPosition, pub position: ParserPosition,
} }
@ -259,7 +264,7 @@ pub struct MethodOutput {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MethodDecorators { pub struct MethodDecorators {
pub description: Option<String>, pub description: Option<String>,
pub parameter_descriptions: HashMap<String, String>, pub parameter_descriptions: BTreeMap<String, String>,
pub return_description: Option<String>, pub return_description: Option<String>,
} }
@ -267,7 +272,7 @@ fn build_type(stmt: &TypeStatement) -> Result<TypeDefinition> {
let mut typedef = TypeDefinition { let mut typedef = TypeDefinition {
position: stmt.position.clone(), position: stmt.position.clone(),
name: stmt.name.clone(), name: stmt.name.clone(),
depends: HashSet::new(), depends: BTreeSet::new(),
fields: Vec::new(), fields: Vec::new(),
}; };
@ -332,7 +337,7 @@ fn build_service(stmt: &ServiceStatement) -> Result<ServiceDefinition> {
let mut servdef = ServiceDefinition { let mut servdef = ServiceDefinition {
position: stmt.position.clone(), position: stmt.position.clone(),
name: stmt.name.clone(), name: stmt.name.clone(),
depends: HashSet::new(), depends: BTreeSet::new(),
methods: Vec::new(), methods: Vec::new(),
}; };
@ -349,7 +354,7 @@ fn build_service(stmt: &ServiceStatement) -> Result<ServiceDefinition> {
}), }),
decorators: MethodDecorators { decorators: MethodDecorators {
description: None, description: None,
parameter_descriptions: HashMap::new(), parameter_descriptions: BTreeMap::new(),
return_description: None, return_description: None,
}, },
}; };
@ -450,7 +455,7 @@ fn build_service(stmt: &ServiceStatement) -> Result<ServiceDefinition> {
} }
pub fn build_ir(root: &Vec<RootNode>) -> Result<IR> { pub fn build_ir(root: &Vec<RootNode>) -> Result<IR> {
let mut options = HashMap::<String, String>::new(); let mut options = BTreeMap::<String, String>::new();
let mut steps = Vec::new(); let mut steps = Vec::new();
for node in root { for node in root {
@ -476,8 +481,8 @@ pub fn build_ir(root: &Vec<RootNode>) -> Result<IR> {
} }
} }
let mut all_types = HashSet::<String>::new(); let mut all_types = BTreeSet::<String>::new();
let mut serv_types = HashSet::<String>::new(); let mut serv_types = BTreeSet::<String>::new();
for bi in &BUILT_INS { for bi in &BUILT_INS {
all_types.insert(bi.to_string()); all_types.insert(bi.to_string());
@ -577,3 +582,36 @@ impl Display for IRError {
write!(f, "ParserError: {} at {:?}", self.message, self.position) write!(f, "ParserError: {} at {:?}", self.message, self.position)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn btreeset_order_is_consistent() {
let mut set1 = BTreeSet::new();
let mut set2 = BTreeSet::new();
let elements = vec![
BaseType::Custom("CustomType".to_string()),
BaseType::Void,
BaseType::Bytes,
BaseType::Float,
];
// Insert in normal order
for elem in &elements {
set1.insert(elem.clone());
}
// Insert in reverse order
for elem in elements.iter().rev() {
set2.insert(elem.clone());
}
let iter1: Vec<_> = set1.iter().cloned().collect();
let iter2: Vec<_> = set2.iter().cloned().collect();
assert_eq!(iter1, iter2); // Order must be the same
}
}

View File

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use log::warn; use log::warn;
use std::collections::{HashMap, HashSet}; use std::collections::{BTreeMap, BTreeSet};
use crate::compile::{Compile, CompileContext, FileGenerator}; use crate::compile::{Compile, CompileContext, FileGenerator};
use crate::ir::{BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition}; use crate::ir::{BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition};
@ -46,7 +46,7 @@ impl RustCompiler {
fn add_dependencies( fn add_dependencies(
&mut self, &mut self,
file: &mut FileGenerator, file: &mut FileGenerator,
depends: &HashSet<BaseType>, depends: &BTreeSet<BaseType>,
) -> Result<()> { ) -> Result<()> {
for dep in depends { for dep in depends {
match dep { match dep {
@ -146,8 +146,9 @@ impl RustCompiler {
f.a0("#[async_trait]"); f.a0("#[async_trait]");
f.a0(format!("pub trait {} {{", definition.name)); f.a0(format!("pub trait {} {{", definition.name));
f.a1("type Context: Clone + Sync + Send + 'static;");
for method in definition.methods.iter() { for method in definition.methods.iter() {
let params = method let mut params = method
.inputs .inputs
.iter() .iter()
.map(|arg| { .map(|arg| {
@ -157,8 +158,9 @@ impl RustCompiler {
Self::type_to_rust_ext(&arg.typ) Self::type_to_rust_ext(&arg.typ)
) )
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>();
.join(", "); params.push("ctx: Self::Context".to_string());
let params = params.join(", ");
let ret = method let ret = method
.output .output
@ -179,17 +181,20 @@ impl RustCompiler {
f.a0("}"); f.a0("}");
f.a0(""); f.a0("");
f.a0(format!("pub struct {}Handler {{", definition.name)); f.a0(format!("pub struct {}Handler<Context> {{", definition.name));
f.a1(format!( f.a1(format!(
"implementation: Box<dyn {} + Sync + Send + 'static>,", "implementation: Box<dyn {}<Context = Context> + Sync + Send + 'static>,",
definition.name definition.name
)); ));
f.a0("}"); f.a0("}");
f.a0(""); f.a0("");
f.a0(format!("impl {}Handler {{", definition.name)); f.a0(format!(
"impl<Context: Clone + Sync + Send + 'static> {}Handler<Context> {{",
definition.name
));
f.a1(format!( f.a1(format!(
"pub fn new(implementation: Box<dyn {} + Sync + Send + 'static>) -> Arc<Self> {{", "pub fn new(implementation: Box<dyn {}<Context = Context> + Sync + Send + 'static>) -> Arc<Self> {{",
definition.name, definition.name,
)); ));
f.a2("Arc::from(Self { implementation })"); f.a2("Arc::from(Self { implementation })");
@ -200,9 +205,10 @@ impl RustCompiler {
f.a0("#[async_trait]"); f.a0("#[async_trait]");
f.a0(format!( f.a0(format!(
"impl JRPCServerService for {}Handler {{", "impl<Context: Clone + Sync + Send + 'static> JRPCServerService for {}Handler<Context> {{",
definition.name definition.name
)); ));
f.a1("type Context = Context;");
f.a1(format!( f.a1(format!(
"fn get_id(&self) -> String {{ \"{}\".to_owned() }} ", "fn get_id(&self) -> String {{ \"{}\".to_owned() }} ",
definition.name definition.name
@ -212,7 +218,7 @@ impl RustCompiler {
f.a1("#[allow(non_snake_case)]"); f.a1("#[allow(non_snake_case)]");
f.a1( f.a1(
"async fn handle(&self, msg: &JRPCRequest, function: &str) -> Result<(bool, Value)> {", "async fn handle(&self, msg: &JRPCRequest, function: &str, ctx: Self::Context) -> Result<(bool, Value)> {",
); );
f.a2("match function {"); f.a2("match function {");
@ -225,7 +231,7 @@ impl RustCompiler {
)); ));
if method.inputs.len() < 1 { if method.inputs.len() < 1 {
f.a5(format!( f.a5(format!(
"let res = self.implementation.{}().await?;", "let res = self.implementation.{}(ctx).await?;",
method.name method.name
)); ));
f.a5("Ok((true, serde_json::to_value(res)?))"); f.a5("Ok((true, serde_json::to_value(res)?))");
@ -249,7 +255,7 @@ impl RustCompiler {
), ),
); );
} }
f.a5(").await?;"); f.a5(",ctx).await?;");
if let Some(_output) = &method.output { if let Some(_output) = &method.output {
f.a5("Ok((true, serde_json::to_value(res)?))"); f.a5("Ok((true, serde_json::to_value(res)?))");
@ -277,7 +283,7 @@ impl RustCompiler {
), ),
); );
} }
f.a5(").await?;"); f.a5(", ctx).await?;");
if let Some(_output) = &method.output { if let Some(_output) = &method.output {
f.a5("Ok((true, serde_json::to_value(res)?))"); f.a5("Ok((true, serde_json::to_value(res)?))");
} else { } else {
@ -403,7 +409,7 @@ impl RustCompiler {
} }
impl Compile for RustCompiler { impl Compile for RustCompiler {
fn new(options: &HashMap<String, String>) -> anyhow::Result<Self> { fn new(options: &BTreeMap<String, String>) -> anyhow::Result<Self> {
let crate_name = if let Some(crate_name) = options.get("rust_crate") { let crate_name = if let Some(crate_name) = options.get("rust_crate") {
crate_name.to_string() crate_name.to_string()
} else { } else {

View File

@ -1,18 +1,13 @@
use anyhow::{anyhow, Result}; use std::collections::{BTreeMap, BTreeSet};
use anyhow::Result;
use log::info; use log::info;
use std::collections::{HashMap, HashSet};
use crate::compile::{Compile, CompileContext, FileGenerator}; use crate::compile::{Compile, CompileContext, FileGenerator};
use crate::ir::{BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition}; use crate::ir::{BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition};
use crate::IR; use crate::IR;
// #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
// pub enum Flavour {
// ESM,
// Node,
// }
pub trait Flavour { pub trait Flavour {
fn ext() -> &'static str; fn ext() -> &'static str;
fn name() -> &'static str; fn name() -> &'static str;
@ -115,9 +110,6 @@ impl<F: Flavour> TypeScriptCompiler<F> {
fn type_to_typescript_ext(typ: &Type) -> String { fn type_to_typescript_ext(typ: &Type) -> String {
let mut result = Self::type_to_typescript(&typ.0); let mut result = Self::type_to_typescript(&typ.0);
let (optional, array, map) = typ.1.get_flags(); let (optional, array, map) = typ.1.get_flags();
if optional {
result = format!("({} | undefined)", result);
}
if array { if array {
result = format!("({})[]", result); result = format!("({})[]", result);
} }
@ -128,13 +120,17 @@ impl<F: Flavour> TypeScriptCompiler<F> {
result result
); );
} }
if optional {
// Optional should be the last modifier
result = format!("({} | undefined)", result);
}
result result
} }
fn add_dependencies( fn add_dependencies(
&mut self, &mut self,
file: &mut FileGenerator, file: &mut FileGenerator,
depends: &HashSet<BaseType>, depends: &BTreeSet<BaseType>,
) -> Result<()> { ) -> Result<()> {
let esm = F::ext(); let esm = F::ext();
file.a0(format!( file.a0(format!(
@ -439,7 +435,7 @@ import {{ VerificationError }} from \"./ts_base{esm}\";
} }
impl<F: Flavour> Compile for TypeScriptCompiler<F> { impl<F: Flavour> Compile for TypeScriptCompiler<F> {
fn new(options: &HashMap<String, String>) -> Result<Self> { fn new(options: &BTreeMap<String, String>) -> Result<Self> {
let flavour = options let flavour = options
.get("flavour") .get("flavour")
.cloned() .cloned()
@ -476,9 +472,12 @@ impl<F: Flavour> Compile for TypeScriptCompiler<F> {
for field in definition.fields.iter() { for field in definition.fields.iter() {
let typ = Self::type_to_typescript_ext(&field.typ); let typ = Self::type_to_typescript_ext(&field.typ);
let opt = if field.typ.is_optional() { "?" } else { "" };
f.a1(format!( f.a1(format!(
"public {}: {};", "public {}{}: {};",
Self::fix_keyword_name(&field.name), Self::fix_keyword_name(&field.name),
opt,
typ typ
)); ));
} }
@ -541,7 +540,7 @@ impl<F: Flavour> Compile for TypeScriptCompiler<F> {
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut f = FileGenerator::new(); let mut f = FileGenerator::new();
self.add_dependencies(&mut f, &HashSet::new())?; self.add_dependencies(&mut f, &BTreeSet::new())?;
f.a0(format!("enum {} {{", definition.name)); f.a0(format!("enum {} {{", definition.name));
for value in &definition.values { for value in &definition.values {

2
libjrpc/templates/CSharp/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
bin/
obj/

View File

@ -6,10 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
int-enum = { version = "0.5.0", features = ["serde", "convert"] } int-enum = { version = "0.5", features = ["serde", "convert"] }
serde = { version = "1.0.147", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1.0.88" serde_json = "1"
nanoid = "0.4.0" nanoid = "0.4"
tokio = { version = "1.22.0", features = ["full"] } tokio = { version = "1", features = ["full"] }
log = "0.4.17" log = "0.4"
async-trait = "0.1.59" async-trait = "0.1"

View File

@ -105,21 +105,27 @@ impl JRPCClient {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait JRPCServerService: Send + Sync + 'static { pub trait JRPCServerService: Send + Sync {
type Context;
fn get_id(&self) -> String; fn get_id(&self) -> String;
async fn handle(&self, request: &JRPCRequest, function: &str) -> Result<(bool, Value)>; async fn handle(
&self,
request: &JRPCRequest,
function: &str,
ctx: Self::Context,
) -> Result<(bool, Value)>;
} }
pub type JRPCServiceHandle = Arc<dyn JRPCServerService>; pub type JRPCServiceHandle<Context> = Arc<dyn JRPCServerService<Context = Context>>;
#[derive(Clone)] #[derive(Clone)]
pub struct JRPCSession { pub struct JRPCSession<Context> {
server: JRPCServer, server: JRPCServer<Context>,
message_sender: Sender<JRPCResult>, message_sender: Sender<JRPCResult>,
} }
impl JRPCSession { impl<Context: Clone + Send + Sync + 'static> JRPCSession<Context> {
pub fn new(server: JRPCServer, sender: Sender<JRPCResult>) -> JRPCSession { pub fn new(server: JRPCServer<Context>, sender: Sender<JRPCResult>) -> Self {
JRPCSession { JRPCSession {
server, server,
message_sender: sender, message_sender: sender,
@ -148,7 +154,7 @@ impl JRPCSession {
} }
} }
pub fn handle_request(&self, request: JRPCRequest) -> () { pub fn handle_request(&self, request: JRPCRequest, ctx: Context) -> () {
let session = self.clone(); let session = self.clone();
tokio::task::spawn(async move { tokio::task::spawn(async move {
info!("Received request: {}", request.method); info!("Received request: {}", request.method);
@ -163,7 +169,7 @@ impl JRPCSession {
let service = session.server.services.get(service); let service = session.server.services.get(service);
if let Some(service) = service { if let Some(service) = service {
let result = service.handle(&request, function).await; let result = service.handle(&request, function, ctx).await;
match result { match result {
Ok((is_send, result)) => { Ok((is_send, result)) => {
if is_send && request.id.is_some() { if is_send && request.id.is_some() {
@ -204,23 +210,23 @@ impl JRPCSession {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct JRPCServer { pub struct JRPCServer<Context> {
services: HashMap<String, JRPCServiceHandle>, services: HashMap<String, JRPCServiceHandle<Context>>,
} }
impl JRPCServer { impl<Context: Clone + Send + Sync + 'static> JRPCServer<Context> {
pub fn new() -> JRPCServer { pub fn new() -> Self {
JRPCServer { JRPCServer {
services: HashMap::new(), services: HashMap::new(),
} }
} }
pub fn add_service(&mut self, service: JRPCServiceHandle) -> () { pub fn add_service(&mut self, service: JRPCServiceHandle<Context>) -> () {
let id = service.get_id(); let id = service.get_id();
self.services.insert(id, service); self.services.insert(id, service);
} }
pub fn get_session(&self, sender: Sender<JRPCResult>) -> JRPCSession { pub fn get_session(&self, sender: Sender<JRPCResult>) -> JRPCSession<Context> {
JRPCSession::new(self.clone(), sender) JRPCSession::new(self.clone(), sender)
} }
} }