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]]
name = "jrpc-cli"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"anyhow",
"clap",

View File

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

View File

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

View File

@ -1,5 +1,5 @@
use std::{
collections::{HashMap, HashSet},
collections::{BTreeMap, BTreeSet},
error::Error,
fmt::{Debug, Display},
hash::{Hash, Hasher},
@ -20,7 +20,7 @@ pub trait Definition: Debug {
#[derive(Debug, Clone)]
pub struct IR {
pub options: HashMap<String, String>,
pub options: BTreeMap<String, String>,
pub steps: Vec<Step>,
}
@ -58,7 +58,12 @@ impl ToString for BaseType {
impl Hash for BaseType {
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)]
pub struct TypeDefinition {
pub name: String,
pub depends: HashSet<BaseType>,
pub depends: BTreeSet<BaseType>,
pub fields: Vec<Field>,
pub position: ParserPosition,
}
@ -223,7 +228,7 @@ pub struct EnumField {
#[derive(Debug, Clone)]
pub struct ServiceDefinition {
pub name: String,
pub depends: HashSet<BaseType>,
pub depends: BTreeSet<BaseType>,
pub methods: Vec<Method>,
pub position: ParserPosition,
}
@ -259,7 +264,7 @@ pub struct MethodOutput {
#[derive(Debug, Clone)]
pub struct MethodDecorators {
pub description: Option<String>,
pub parameter_descriptions: HashMap<String, String>,
pub parameter_descriptions: BTreeMap<String, String>,
pub return_description: Option<String>,
}
@ -267,7 +272,7 @@ fn build_type(stmt: &TypeStatement) -> Result<TypeDefinition> {
let mut typedef = TypeDefinition {
position: stmt.position.clone(),
name: stmt.name.clone(),
depends: HashSet::new(),
depends: BTreeSet::new(),
fields: Vec::new(),
};
@ -332,7 +337,7 @@ fn build_service(stmt: &ServiceStatement) -> Result<ServiceDefinition> {
let mut servdef = ServiceDefinition {
position: stmt.position.clone(),
name: stmt.name.clone(),
depends: HashSet::new(),
depends: BTreeSet::new(),
methods: Vec::new(),
};
@ -349,7 +354,7 @@ fn build_service(stmt: &ServiceStatement) -> Result<ServiceDefinition> {
}),
decorators: MethodDecorators {
description: None,
parameter_descriptions: HashMap::new(),
parameter_descriptions: BTreeMap::new(),
return_description: None,
},
};
@ -450,7 +455,7 @@ fn build_service(stmt: &ServiceStatement) -> Result<ServiceDefinition> {
}
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();
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 serv_types = HashSet::<String>::new();
let mut all_types = BTreeSet::<String>::new();
let mut serv_types = BTreeSet::<String>::new();
for bi in &BUILT_INS {
all_types.insert(bi.to_string());
@ -577,3 +582,36 @@ impl Display for IRError {
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 log::warn;
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, BTreeSet};
use crate::compile::{Compile, CompileContext, FileGenerator};
use crate::ir::{BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition};
@ -46,7 +46,7 @@ impl RustCompiler {
fn add_dependencies(
&mut self,
file: &mut FileGenerator,
depends: &HashSet<BaseType>,
depends: &BTreeSet<BaseType>,
) -> Result<()> {
for dep in depends {
match dep {
@ -146,8 +146,9 @@ impl RustCompiler {
f.a0("#[async_trait]");
f.a0(format!("pub trait {} {{", definition.name));
f.a1("type Context: Clone + Sync + Send + 'static;");
for method in definition.methods.iter() {
let params = method
let mut params = method
.inputs
.iter()
.map(|arg| {
@ -157,8 +158,9 @@ impl RustCompiler {
Self::type_to_rust_ext(&arg.typ)
)
})
.collect::<Vec<String>>()
.join(", ");
.collect::<Vec<String>>();
params.push("ctx: Self::Context".to_string());
let params = params.join(", ");
let ret = method
.output
@ -179,17 +181,20 @@ impl RustCompiler {
f.a0("}");
f.a0("");
f.a0(format!("pub struct {}Handler {{", definition.name));
f.a0(format!("pub struct {}Handler<Context> {{", definition.name));
f.a1(format!(
"implementation: Box<dyn {} + Sync + Send + 'static>,",
"implementation: Box<dyn {}<Context = Context> + Sync + Send + 'static>,",
definition.name
));
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!(
"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,
));
f.a2("Arc::from(Self { implementation })");
@ -200,9 +205,10 @@ impl RustCompiler {
f.a0("#[async_trait]");
f.a0(format!(
"impl JRPCServerService for {}Handler {{",
"impl<Context: Clone + Sync + Send + 'static> JRPCServerService for {}Handler<Context> {{",
definition.name
));
f.a1("type Context = Context;");
f.a1(format!(
"fn get_id(&self) -> String {{ \"{}\".to_owned() }} ",
definition.name
@ -212,7 +218,7 @@ impl RustCompiler {
f.a1("#[allow(non_snake_case)]");
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 {");
@ -225,7 +231,7 @@ impl RustCompiler {
));
if method.inputs.len() < 1 {
f.a5(format!(
"let res = self.implementation.{}().await?;",
"let res = self.implementation.{}(ctx).await?;",
method.name
));
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 {
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 {
f.a5("Ok((true, serde_json::to_value(res)?))");
} else {
@ -403,7 +409,7 @@ impl 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") {
crate_name.to_string()
} else {

View File

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

View File

@ -105,21 +105,27 @@ impl JRPCClient {
}
#[async_trait::async_trait]
pub trait JRPCServerService: Send + Sync + 'static {
pub trait JRPCServerService: Send + Sync {
type Context;
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)]
pub struct JRPCSession {
server: JRPCServer,
pub struct JRPCSession<Context> {
server: JRPCServer<Context>,
message_sender: Sender<JRPCResult>,
}
impl JRPCSession {
pub fn new(server: JRPCServer, sender: Sender<JRPCResult>) -> JRPCSession {
impl<Context: Clone + Send + Sync + 'static> JRPCSession<Context> {
pub fn new(server: JRPCServer<Context>, sender: Sender<JRPCResult>) -> Self {
JRPCSession {
server,
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();
tokio::task::spawn(async move {
info!("Received request: {}", request.method);
@ -163,7 +169,7 @@ impl JRPCSession {
let service = session.server.services.get(service);
if let Some(service) = service {
let result = service.handle(&request, function).await;
let result = service.handle(&request, function, ctx).await;
match result {
Ok((is_send, result)) => {
if is_send && request.id.is_some() {
@ -204,23 +210,23 @@ impl JRPCSession {
}
#[derive(Clone)]
pub struct JRPCServer {
services: HashMap<String, JRPCServiceHandle>,
pub struct JRPCServer<Context> {
services: HashMap<String, JRPCServiceHandle<Context>>,
}
impl JRPCServer {
pub fn new() -> JRPCServer {
impl<Context: Clone + Send + Sync + 'static> JRPCServer<Context> {
pub fn new() -> Self {
JRPCServer {
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();
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)
}
}