├── src ├── params │ ├── mod.rs │ └── into_params.rs ├── rpc_message │ ├── mod.rs │ ├── support.rs │ ├── rpc_request_parsing_error.rs │ ├── request.rs │ └── notification.rs ├── resource │ ├── mod.rs │ ├── resources_builder_macro.rs │ ├── from_resources.rs │ ├── resources.rs │ └── resources_inner.rs ├── rpc_response │ ├── mod.rs │ ├── rpc_response_parsing_error.rs │ ├── rpc_error.rs │ └── response.rs ├── router │ ├── mod.rs │ ├── call_success.rs │ ├── call_error.rs │ ├── router_builder_macro.rs │ ├── router_inner.rs │ ├── router_builder.rs │ └── router.rs ├── handler │ ├── mod.rs │ ├── handler.rs │ ├── handler_wrapper.rs │ ├── handler_error.rs │ └── impl_handlers.rs ├── support.rs ├── error.rs ├── lib.rs └── rpc_id.rs ├── .gitignore ├── rustfmt.toml ├── rpc-router-macros ├── Cargo.toml └── src │ ├── derive_resource.rs │ ├── derive_handler_error.rs │ ├── lib.rs │ └── derive_params.rs ├── Cargo.toml ├── tests ├── test-base-resources.rs ├── test-calls.rs └── test-custom-error.rs ├── examples ├── c01-simple.rs ├── c02-multi-calls.rs ├── c03-with-derives.rs ├── c00-readme.rs └── c05-error-handling.rs ├── CHANGELOG.md ├── doc ├── README.md └── all-apis.md └── README.md /src/params/mod.rs: -------------------------------------------------------------------------------- 1 | // region: --- Modules 2 | 3 | mod into_params; 4 | 5 | // -- Flatten 6 | pub use into_params::*; 7 | 8 | // endregion: --- Modules 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -- Base 2 | .* 3 | !.gitignore 4 | 5 | *.lock 6 | *.log 7 | 8 | # -- Rust 9 | target/ 10 | # !Cargo.lock # Commented by default 11 | !.cargo/ 12 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # rustfmt doc - https://rust-lang.github.io/rustfmt/ 2 | 3 | # JC Style 4 | hard_tabs = true # no comment 5 | edition = "2024" 6 | max_width = 120 7 | chain_width = 80 8 | array_width = 80 9 | 10 | # imports_granularity = "Module" # no effect on rust analyzer 11 | -------------------------------------------------------------------------------- /src/rpc_message/mod.rs: -------------------------------------------------------------------------------- 1 | // region: --- Modules 2 | 3 | mod notification; 4 | mod request; 5 | mod rpc_request_parsing_error; 6 | mod support; 7 | 8 | pub use notification::*; 9 | pub use request::*; 10 | pub use rpc_request_parsing_error::*; 11 | 12 | // endregion: --- Modules 13 | -------------------------------------------------------------------------------- /src/resource/mod.rs: -------------------------------------------------------------------------------- 1 | // region: --- Modules 2 | 3 | mod from_resources; 4 | mod resources; 5 | mod resources_builder_macro; 6 | mod resources_inner; 7 | 8 | // -- Flatten 9 | pub use from_resources::*; 10 | pub use resources::*; 11 | pub(crate) use resources_inner::ResourcesInner; 12 | 13 | // endregion: --- Modules 14 | -------------------------------------------------------------------------------- /src/rpc_response/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_inception)] // not publicly exposed 2 | 3 | // region: --- Modules 4 | 5 | mod response; 6 | mod rpc_error; 7 | mod rpc_response_parsing_error; 8 | 9 | // -- Flatten 10 | pub use response::*; 11 | pub use rpc_error::*; 12 | pub use rpc_response_parsing_error::*; 13 | 14 | // endregion: --- Modules 15 | -------------------------------------------------------------------------------- /src/router/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_inception)] // not publicly exposed 2 | 3 | // region: --- Modules 4 | 5 | mod call_error; 6 | mod call_success; 7 | mod router; 8 | mod router_builder; 9 | mod router_builder_macro; 10 | mod router_inner; 11 | 12 | // -- Flatten 13 | pub use call_error::*; 14 | pub use call_success::*; 15 | pub use router::*; 16 | pub use router_builder::*; 17 | 18 | // endregion: --- Modules 19 | -------------------------------------------------------------------------------- /src/handler/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_inception)] // not publicly exposed 2 | 3 | // region: --- Modules 4 | 5 | mod handler; 6 | mod handler_error; 7 | mod handler_wrapper; 8 | mod impl_handlers; 9 | 10 | // -- Flatten 11 | pub use handler::*; 12 | pub use handler_error::*; 13 | pub use handler_wrapper::*; 14 | 15 | use futures::Future; 16 | use serde_json::Value; 17 | use std::pin::Pin; 18 | 19 | // endregion: --- Modules 20 | 21 | type PinFutureValue = Pin> + Send>>; 22 | -------------------------------------------------------------------------------- /src/router/call_success.rs: -------------------------------------------------------------------------------- 1 | use crate::RpcId; 2 | use serde_json::Value; 3 | 4 | /// The successful response back from a `rpc_router.call...` functions. 5 | /// 6 | /// NOTE: CallSuccess & CallError 7 | /// are not designed to be the JSON-RPC Response 8 | /// or Error, but to provide the necessary context 9 | /// to build those, as well as the useful `method name` 10 | /// context for tracing/login. 11 | #[derive(Debug, Clone)] 12 | pub struct CallSuccess { 13 | pub id: RpcId, 14 | pub method: String, 15 | pub value: Value, 16 | } 17 | -------------------------------------------------------------------------------- /rpc-router-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rpc-router-macros" 3 | version = "0.2.0-alpha.1" 4 | authors = ["jeremy.chone@gmail.com"] 5 | edition = "2024" 6 | license = "MIT OR Apache-2.0" 7 | description = "Proc Macros for rpc-router crate" 8 | keywords = [ 9 | "rpc", 10 | "json-rpc", 11 | ] 12 | homepage = "https://github.com/jeremychone/rust-rpc-router" 13 | repository = "https://github.com/jeremychone/rust-rpc-router" 14 | 15 | [lints] 16 | workspace = true 17 | 18 | [dependencies] 19 | quote = "1" 20 | syn = {version = "2", features = ["full"]} 21 | 22 | [lib] 23 | proc-macro = true -------------------------------------------------------------------------------- /rpc-router-macros/src/derive_resource.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{DeriveInput, parse_macro_input}; 4 | 5 | pub fn derive_rpc_resource_inner(input: TokenStream) -> TokenStream { 6 | // Parse the input tokens into a syntax tree 7 | let input = parse_macro_input!(input as DeriveInput); 8 | 9 | // Build the impl 10 | let name = input.ident; // Gets the identifier of the enum/struct 11 | let expanded = quote! { 12 | // Generate the trait implementation 13 | impl rpc_router::FromResources for #name {} 14 | }; 15 | 16 | // Convert back to a token stream and return it 17 | TokenStream::from(expanded) 18 | } 19 | -------------------------------------------------------------------------------- /rpc-router-macros/src/derive_handler_error.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{DeriveInput, parse_macro_input}; 4 | 5 | pub fn drive_rpc_handler_error_inner(input: TokenStream) -> TokenStream { 6 | // Parse the input tokens into a syntax tree 7 | let input = parse_macro_input!(input as DeriveInput); 8 | 9 | // Build the impl 10 | let name = input.ident; // Gets the identifier of the enum/struct 11 | let expanded = quote! { 12 | // Generate the trait implementation 13 | impl rpc_router::IntoHandlerError for #name {} 14 | }; 15 | 16 | // Convert back to a token stream and return it 17 | TokenStream::from(expanded) 18 | } 19 | -------------------------------------------------------------------------------- /src/support.rs: -------------------------------------------------------------------------------- 1 | use derive_more::Display; 2 | use serde_json::Value; 3 | 4 | // region: --- Serde Value Util 5 | 6 | #[derive(Clone, Debug, Display)] 7 | pub enum JsonType { 8 | Null, 9 | Bool, 10 | Integer, 11 | Unsigned, 12 | Float, 13 | String, 14 | Array, 15 | Object, 16 | } 17 | 18 | pub fn get_json_type(value: &Value) -> JsonType { 19 | match value { 20 | Value::Null => JsonType::Null, 21 | Value::Bool(_) => JsonType::Bool, 22 | Value::Number(n) => { 23 | if n.is_i64() { 24 | JsonType::Integer 25 | } else if n.is_u64() { 26 | JsonType::Unsigned 27 | } else if n.is_f64() { 28 | JsonType::Float 29 | } else { 30 | unreachable!("serde_json::Number should be i64, u64, or f64"); 31 | } 32 | } 33 | Value::String(_) => JsonType::String, 34 | Value::Array(_) => JsonType::Array, 35 | Value::Object(_) => JsonType::Object, 36 | } 37 | } 38 | 39 | // endregion: --- Serde Value Util 40 | -------------------------------------------------------------------------------- /src/router/call_error.rs: -------------------------------------------------------------------------------- 1 | use crate::{CallSuccess, RpcId}; 2 | 3 | pub type CallResult = core::result::Result; 4 | 5 | /// The Error type returned by `rpc_router.call...` functions. 6 | /// 7 | /// NOTE: CallSuccess & CallError 8 | /// are not designed to be the JSON-RPC Response 9 | /// or Error, but to provide the necessary context 10 | /// to build those, as well as the useful `method name` 11 | /// context for tracing/login. 12 | #[derive(Debug)] 13 | pub struct CallError { 14 | pub id: RpcId, 15 | pub method: String, 16 | pub error: crate::Error, 17 | } 18 | 19 | // region: --- Error Boilerplate 20 | 21 | impl core::fmt::Display for CallError { 22 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { 23 | write!(fmt, "{self:?}") 24 | } 25 | } 26 | 27 | impl std::error::Error for CallError {} 28 | 29 | // endregion: --- Error Boilerplate 30 | -------------------------------------------------------------------------------- /src/resource/resources_builder_macro.rs: -------------------------------------------------------------------------------- 1 | /// A simple macro to create a new RpcRouterInner 2 | /// and add each rpc handler-compatible function along with their corresponding names. 3 | /// 4 | /// e.g., 5 | /// 6 | /// ``` 7 | /// rpc_router!( 8 | /// create_project, 9 | /// list_projects, 10 | /// update_project, 11 | /// delete_project 12 | /// ); 13 | /// ``` 14 | /// Is equivalent to: 15 | /// ``` 16 | /// RpcRouterBuilder::default() 17 | /// .append_dyn("create_project", create_project.into_box()) 18 | /// .append_dyn("list_projects", list_projects.into_box()) 19 | /// .append_dyn("update_project", update_project.into_box()) 20 | /// .append_dyn("delete_project", delete_project.into_box()) 21 | /// ``` 22 | #[macro_export] 23 | macro_rules! resources_builder { 24 | ($($x:expr),*) => { 25 | { 26 | let mut temp = rpc_router::ResourcesBuilder::default(); 27 | $( 28 | temp = temp.append($x); 29 | )* 30 | temp 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /rpc-router-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | // region: --- Modules 2 | 3 | mod derive_handler_error; 4 | mod derive_params; 5 | mod derive_resource; 6 | 7 | use proc_macro::TokenStream; 8 | 9 | use crate::derive_handler_error::drive_rpc_handler_error_inner; 10 | use crate::derive_params::derive_rpc_params_inner; 11 | use crate::derive_resource::derive_rpc_resource_inner; 12 | 13 | // endregion: --- Modules 14 | 15 | /// Will implement `IntoHandlerError` for this target type. 16 | /// The target type must implement `std::error::Error` 17 | #[proc_macro_derive(RpcHandlerError)] 18 | pub fn derive_rpc_handler_error(input: TokenStream) -> TokenStream { 19 | drive_rpc_handler_error_inner(input) 20 | } 21 | 22 | /// Will implement `IntoParams` for this target type. 23 | /// The target type must implement `Deserialize` 24 | #[proc_macro_derive(RpcParams)] 25 | pub fn derive_rpc_params(input: TokenStream) -> TokenStream { 26 | derive_rpc_params_inner(input) 27 | } 28 | 29 | /// Will implement `FromResources` for this target type. 30 | /// The target type must implement `Clone + Send + Sync` 31 | #[proc_macro_derive(RpcResource)] 32 | pub fn derive_rpc_resource(input: TokenStream) -> TokenStream { 33 | derive_rpc_resource_inner(input) 34 | } 35 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::{FromResourcesError, HandlerError}; 2 | use serde::Serialize; 3 | use serde_with::{DisplayFromStr, serde_as}; 4 | 5 | pub type Result = core::result::Result; 6 | 7 | #[serde_as] 8 | #[derive(Debug, Serialize)] 9 | pub enum Error { 10 | // -- Into Params 11 | ParamsParsing(#[serde_as(as = "DisplayFromStr")] serde_json::Error), 12 | ParamsMissingButRequested, 13 | 14 | // -- Router 15 | MethodUnknown, 16 | 17 | // -- Handler 18 | FromResources(FromResourcesError), 19 | HandlerResultSerialize(#[serde_as(as = "DisplayFromStr")] serde_json::Error), 20 | Handler(#[serde_as(as = "DisplayFromStr")] HandlerError), 21 | } 22 | 23 | // region: --- Froms 24 | 25 | impl From for Error { 26 | fn from(val: HandlerError) -> Self { 27 | Self::Handler(val) 28 | } 29 | } 30 | 31 | impl From for Error { 32 | fn from(val: FromResourcesError) -> Self { 33 | Self::FromResources(val) 34 | } 35 | } 36 | 37 | // endregion: --- Froms 38 | 39 | // region: --- Error Boilerplate 40 | 41 | impl core::fmt::Display for Error { 42 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { 43 | write!(fmt, "{self:?}") 44 | } 45 | } 46 | 47 | impl std::error::Error for Error {} 48 | 49 | // endregion: --- Error Boilerplate 50 | -------------------------------------------------------------------------------- /rpc-router-macros/src/derive_params.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{DeriveInput, GenericParam, parse_macro_input}; 4 | 5 | pub fn derive_rpc_params_inner(input: TokenStream) -> TokenStream { 6 | let input = parse_macro_input!(input as DeriveInput); 7 | let name = &input.ident; 8 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 9 | 10 | let expanded = if input.generics.params.is_empty() { 11 | // Non-generic struct 12 | quote! { 13 | impl rpc_router::IntoParams for #name {} 14 | } 15 | } else { 16 | // Generic struct 17 | // Extend where_clause with DeserializeOwned + Send for each type parameter 18 | let where_clause = where_clause.map_or_else( 19 | || { 20 | let constraints = input.generics.params.iter().filter_map(|p| { 21 | if let GenericParam::Type(type_param) = p { 22 | Some(quote! { #type_param: serde::de::DeserializeOwned + Send }) 23 | } else { 24 | None 25 | } 26 | }); 27 | quote! { where #(#constraints,)* } 28 | }, 29 | |where_clause| quote! { #where_clause }, 30 | ); 31 | 32 | quote! { 33 | impl #impl_generics rpc_router::IntoParams for #name #ty_generics #where_clause {} 34 | } 35 | }; 36 | // Convert back to a token stream and return it 37 | TokenStream::from(expanded) 38 | } 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rpc-router" 3 | version = "0.2.0-alpha.2-wip" 4 | authors = ["Jeremy Chone "] 5 | edition = "2024" 6 | license = "MIT OR Apache-2.0" 7 | description = "JSON RPC Router Library" 8 | keywords = [ 9 | "rpc", 10 | "json-rpc", 11 | ] 12 | homepage = "https://github.com/jeremychone/rust-rpc-router" 13 | repository = "https://github.com/jeremychone/rust-rpc-router" 14 | 15 | [workspace.lints.rust] 16 | unsafe_code = "forbid" 17 | # unused = "allow" # For experimental dev. 18 | 19 | [lints.rust] 20 | # unused = "allow" # For tests/*.rs 21 | 22 | [lib] 23 | doctest = false 24 | 25 | [workspace] 26 | members = [".", "rpc-router-macros"] 27 | 28 | [features] 29 | default = ["rpc-router-macros"] 30 | 31 | [dependencies] 32 | # -- Async 33 | futures = "0.3" 34 | # -- Json 35 | serde = { version = "1", features = ["derive"] } 36 | serde_json = "1" 37 | serde_with = "3" 38 | # -- uuid & encoding 39 | uuid = {version = "1", features = ["v4", "v7", "fast-rng"]} 40 | data-encoding = "2.5" # base64, base64url, base32hex 41 | bs58 = "0.5" 42 | # -- Features 43 | rpc-router-macros = { version="=0.2.0-alpha.1", path = "rpc-router-macros", optional=true} 44 | # -- Others 45 | derive_more = {version = "2", features = ["from", "display"] } 46 | bitflags = "2.9.0" 47 | 48 | [dev-dependencies] 49 | # -- Async 50 | tokio = { version = "1", features = ["full"] } 51 | -------------------------------------------------------------------------------- /src/resource/from_resources.rs: -------------------------------------------------------------------------------- 1 | use crate::Resources; 2 | use serde::Serialize; 3 | use std::any::type_name; 4 | 5 | pub trait FromResources { 6 | fn from_resources(resources: &Resources) -> FromResourcesResult 7 | where 8 | Self: Sized + Clone + Send + Sync + 'static, 9 | { 10 | resources 11 | .get::() 12 | .ok_or_else(FromResourcesError::resource_not_found::) 13 | } 14 | } 15 | 16 | /// Implements `FromResources` to allow requesting Option 17 | /// when T implements FromResources. 18 | impl FromResources for Option 19 | where 20 | T: FromResources, 21 | T: Sized + Clone + Send + Sync + 'static, 22 | { 23 | fn from_resources(resources: &Resources) -> FromResourcesResult { 24 | Ok(resources.get::()) 25 | } 26 | } 27 | 28 | // region: --- Error 29 | 30 | pub type FromResourcesResult = core::result::Result; 31 | 32 | #[derive(Debug, Serialize)] 33 | pub enum FromResourcesError { 34 | ResourceNotFound(&'static str), 35 | } 36 | 37 | impl FromResourcesError { 38 | pub fn resource_not_found() -> FromResourcesError { 39 | let name: &'static str = type_name::(); 40 | Self::ResourceNotFound(name) 41 | } 42 | } 43 | 44 | impl core::fmt::Display for FromResourcesError { 45 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { 46 | write!(fmt, "{self:?}") 47 | } 48 | } 49 | 50 | impl std::error::Error for FromResourcesError {} 51 | 52 | // endregion: --- Error 53 | -------------------------------------------------------------------------------- /tests/test-base-resources.rs: -------------------------------------------------------------------------------- 1 | pub type Result = core::result::Result; 2 | pub type Error = Box; // For early dev. 3 | 4 | use rpc_router::{HandlerResult, RpcParams, RpcResource, router_builder}; 5 | use serde::Deserialize; 6 | use serde_json::json; 7 | use tokio::task::JoinSet; 8 | 9 | // region: --- Test Assets 10 | 11 | #[derive(Clone, RpcResource)] 12 | pub struct ModelManager; 13 | 14 | #[derive(Clone, RpcResource)] 15 | pub struct AiManager; 16 | 17 | #[derive(Deserialize, RpcParams)] 18 | pub struct ParamsIded { 19 | pub id: i64, 20 | } 21 | 22 | pub async fn get_task(_mm: ModelManager, _aim: AiManager, params: ParamsIded) -> HandlerResult { 23 | Ok(params.id + 9000) 24 | } 25 | 26 | // endregion: --- Test Assets 27 | 28 | #[tokio::test] 29 | async fn test_base_resources() -> Result<()> { 30 | // -- Setup & Fixtures 31 | let fx_num = 125; 32 | let fx_res_value = 9125; 33 | let rpc_router = router_builder!( 34 | handlers: [get_task], 35 | resources: [ModelManager, AiManager] 36 | ) 37 | .build(); 38 | 39 | // -- spawn calls 40 | let mut joinset = JoinSet::new(); 41 | for _ in 0..2 { 42 | let rpc_router = rpc_router.clone(); 43 | joinset.spawn(async move { 44 | let rpc_router = rpc_router.clone(); 45 | 46 | let params = json!({"id": fx_num}); 47 | 48 | rpc_router.call_route(None, "get_task", Some(params)).await 49 | }); 50 | } 51 | 52 | // -- Check 53 | while let Some(res) = joinset.join_next().await { 54 | let res = res??; 55 | let res_value: i32 = serde_json::from_value(res.value)?; 56 | assert_eq!(res_value, fx_res_value); 57 | } 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /src/rpc_response/rpc_response_parsing_error.rs: -------------------------------------------------------------------------------- 1 | use crate::RpcId; 2 | use serde::Serialize; 3 | use serde_json::Value; 4 | use serde_with::{serde_as, DisplayFromStr}; 5 | 6 | /// Error type for failures during `RpcResponse` parsing or validation. 7 | #[serde_as] 8 | #[derive(Debug, Serialize)] 9 | pub enum RpcResponseParsingError { 10 | InvalidJsonRpcVersion { 11 | id: Option, 12 | expected: &'static str, 13 | actual: Option, 14 | }, 15 | MissingJsonRpcVersion { 16 | id: Option, 17 | }, 18 | MissingId, 19 | InvalidId(#[serde_as(as = "DisplayFromStr")] crate::RpcRequestParsingError), 20 | MissingResultAndError { 21 | id: RpcId, 22 | }, 23 | BothResultAndError { 24 | id: RpcId, 25 | }, 26 | InvalidErrorObject(#[serde_as(as = "DisplayFromStr")] serde_json::Error), 27 | Serde(#[serde_as(as = "DisplayFromStr")] serde_json::Error), 28 | } 29 | 30 | // region: --- Error Boilerplate 31 | 32 | impl core::fmt::Display for RpcResponseParsingError { 33 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { 34 | write!(fmt, "{self:?}") 35 | } 36 | } 37 | 38 | impl std::error::Error for RpcResponseParsingError {} 39 | 40 | // endregion: --- Error Boilerplate 41 | 42 | // region: --- Froms 43 | 44 | impl From for RpcResponseParsingError { 45 | fn from(e: serde_json::Error) -> Self { 46 | RpcResponseParsingError::Serde(e) 47 | } 48 | } 49 | 50 | impl From for RpcResponseParsingError { 51 | fn from(e: crate::RpcRequestParsingError) -> Self { 52 | RpcResponseParsingError::InvalidId(e) 53 | } 54 | } 55 | 56 | // endregion: --- Froms 57 | -------------------------------------------------------------------------------- /src/handler/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::handler::{RpcHandlerWrapper, RpcHandlerWrapperTrait}; 2 | use crate::{Resources, Result}; 3 | use futures::Future; 4 | use serde_json::Value; 5 | 6 | /// The `Handler` trait that will be implemented by rpc handler functions. 7 | /// 8 | /// Key points: 9 | /// - Rpc handler functions are asynchronous, thus returning a Future of Result. 10 | /// - The call format is normalized to two `impl FromResources` arguments (for now) and one optionals `impl IntoParams`, which represent the json-rpc's optional value. 11 | /// - `into_box` is a convenient method for converting a RpcHandler into a Boxed dyn RpcHandlerWrapperTrait, 12 | /// allowing for dynamic dispatch by the Router. 13 | /// - A `RpcHandler` will typically be implemented for static functions, as `FnOnce`, 14 | /// enabling them to be cloned with none or negligible performance impact, 15 | /// thus facilitating the use of RpcRoute dynamic dispatch. 16 | /// - `T` is the tuple of `impl FromResources` arguments. 17 | /// - `P` is the `impl IntoParams` argument. 18 | /// 19 | pub trait Handler: Clone 20 | where 21 | T: Send + Sync + 'static, 22 | P: Send + Sync + 'static, 23 | R: Send + Sync + 'static, 24 | { 25 | /// The type of future calling this handler returns. 26 | type Future: Future> + Send + 'static; 27 | 28 | /// Call the handler. 29 | fn call(self, rpc_resources: Resources, params: Option) -> Self::Future; 30 | 31 | /// Convert this RpcHandler into a Boxed dyn RpcHandlerWrapperTrait, 32 | /// for dynamic dispatch by the Router. 33 | fn into_dyn(self) -> Box 34 | where 35 | Self: Sized + Send + Sync + 'static, 36 | { 37 | Box::new(RpcHandlerWrapper::new(self)) as Box 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/c01-simple.rs: -------------------------------------------------------------------------------- 1 | use rpc_router::{FromResources, Handler, HandlerResult, IntoParams, Resources, Router, RpcRequest}; 2 | use serde::Deserialize; 3 | use serde_json::json; 4 | 5 | #[derive(Clone)] 6 | pub struct ModelManager {} 7 | impl FromResources for ModelManager {} 8 | 9 | #[derive(Deserialize)] 10 | pub struct ParamsIded { 11 | pub id: i64, 12 | } 13 | impl IntoParams for ParamsIded {} 14 | 15 | pub async fn increment_id(_mm: ModelManager, params: ParamsIded) -> HandlerResult { 16 | Ok(params.id + 9000) 17 | } 18 | 19 | #[tokio::main] 20 | async fn main() -> Result<(), Box> { 21 | // -- Build the Router with the builder 22 | let rpc_router = Router::builder() 23 | // Minor optimization over `.append(...)` to avoid monomorphization 24 | .append_dyn("increment_id", increment_id.into_dyn()) 25 | .build(); 26 | 27 | // -- Build the reqeust 28 | let rpc_request: RpcRequest = json!({ 29 | "jsonrpc": "2.0", 30 | "id": null, // the json rpc id, that will get echoed back, can be null 31 | "method": "increment_id", 32 | "params": { 33 | "id": 123 34 | } 35 | }) 36 | .try_into()?; 37 | 38 | // -- Build the Resources for this call via the builer 39 | let rpc_resources = Resources::builder().append(ModelManager {}).build(); 40 | 41 | // -- Execute 42 | let call_result = rpc_router.call_with_resources(rpc_request, rpc_resources).await; 43 | 44 | // -- Display result 45 | match call_result { 46 | Ok(call_response) => println!( 47 | "Success: rpc-id {:?}, method: {}, returned value: {:?}", 48 | call_response.id, call_response.method, call_response.value 49 | ), 50 | Err(call_error) => println!( 51 | "Error: rpc-id {:?}, method: {}, error {:?}", 52 | call_error.id, call_error.method, call_error.error 53 | ), 54 | // To extract app error type, see code below (examples/c00-readme.md) 55 | } 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /src/rpc_message/support.rs: -------------------------------------------------------------------------------- 1 | //! Support module for parsing shared elements of RpcRequest and RpcNotification. 2 | 3 | use crate::rpc_message::rpc_request_parsing_error::RpcRequestParsingError; 4 | use crate::support::get_json_type; 5 | use serde_json::{Map, Value}; 6 | 7 | /// Extracts a value from the map, removing it. 8 | pub(super) fn extract_value(obj: &mut Map, key: &str) -> Option { 9 | obj.remove(key) 10 | } 11 | 12 | /// Validates the "jsonrpc" property value. 13 | /// Returns `Ok(())` if valid ("2.0"), otherwise returns the invalid value for error reporting. 14 | pub(super) fn validate_version(version_val: Option) -> Result<(), Option> { 15 | match version_val { 16 | Some(version) => { 17 | if version.as_str().unwrap_or_default() == "2.0" { 18 | Ok(()) 19 | } else { 20 | Err(Some(version)) // Invalid version value 21 | } 22 | } 23 | None => Err(None), // Version missing 24 | } 25 | } 26 | 27 | /// Parses the "method" property value. 28 | /// Returns `Ok(String)` if valid, otherwise returns the invalid value for error reporting. 29 | pub(super) fn parse_method(method_val: Option) -> Result> { 30 | match method_val { 31 | Some(Value::String(method_name)) => Ok(method_name), 32 | Some(other) => Err(Some(other)), // Invalid type 33 | None => Err(None), // Method missing 34 | } 35 | } 36 | 37 | /// Parses the "params" property value. 38 | /// Params can be absent, an array, or an object. 39 | pub(super) fn parse_params(params_val: Option) -> Result, RpcRequestParsingError> { 40 | match params_val { 41 | None => Ok(None), 42 | Some(Value::Array(_)) | Some(Value::Object(_)) => Ok(Some(params_val.unwrap())), // Take ownership 43 | Some(other) => Err(RpcRequestParsingError::ParamsInvalidType { 44 | actual_type: get_json_type(&other).to_string(), 45 | }), 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/c02-multi-calls.rs: -------------------------------------------------------------------------------- 1 | pub type Result = core::result::Result; 2 | pub type Error = Box; // For early dev. 3 | 4 | use rpc_router::{Handler, IntoParams, Resources, Router, RpcRequest, RpcResource}; 5 | use serde::Deserialize; 6 | use serde_json::json; 7 | use tokio::task::JoinSet; 8 | 9 | #[derive(Clone, RpcResource)] 10 | pub struct ModelManager; 11 | 12 | #[derive(Clone, RpcResource)] 13 | pub struct UserCtx { 14 | _user_id: i64, 15 | } 16 | 17 | #[derive(Deserialize)] 18 | pub struct ParamsIded { 19 | pub id: i64, 20 | } 21 | impl IntoParams for ParamsIded {} 22 | 23 | pub async fn get_task(_ctx: UserCtx, _mm: ModelManager, params: ParamsIded) -> rpc_router::HandlerResult { 24 | Ok(params.id + 9000) 25 | } 26 | 27 | #[tokio::main] 28 | async fn main() -> Result<()> { 29 | // -- router 30 | let rpc_router = Router::builder() 31 | .append_dyn("get_task", get_task.into_dyn()) 32 | .append_resource(ModelManager) 33 | .build(); 34 | 35 | // -- spawn calls 36 | let mut joinset = JoinSet::new(); 37 | for idx in 0..2 { 38 | let rpc_router = rpc_router.clone(); 39 | let rpc_request: RpcRequest = json!({ 40 | "jsonrpc": "2.0", 41 | "id": idx, // the json rpc id, that will get echoed back, can be null 42 | "method": "get_task", 43 | "params": { 44 | "id": 123 45 | } 46 | }) 47 | .try_into()?; 48 | 49 | joinset.spawn(async move { 50 | // Cheap way to "ensure" start spawns matches join_next order. (not for prod) 51 | tokio::time::sleep(std::time::Duration::from_millis(idx as u64 * 10)).await; 52 | 53 | // Build the additional resources to overlay on top of the router resources 54 | let addtional_resources = Resources::builder().append(UserCtx { _user_id: 123 }).build(); 55 | 56 | // Exec the call 57 | rpc_router.call_with_resources(rpc_request, addtional_resources).await 58 | }); 59 | } 60 | 61 | // -- print results 62 | // Should have id: 0, and then, id: 1 63 | while let Some(response) = joinset.join_next().await { 64 | println!("res {response:?}"); 65 | } 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! rpc::router module provides the type and implementation for 2 | //! json rpc routing. 3 | //! 4 | //! It has the following constructs: 5 | //! 6 | //! - `Router` holds the HashMap of `method_name: Box`. 7 | //! - `RpcHandler` trait is implemented for any async function that, with 8 | //! `(S1, S2, ...[impl IntoParams])`, returns `web::Result` where S1, S2, ... are 9 | //! types that implement `FromResources` (see router/from_resources.rs and src/resources.rs). 10 | //! - `IntoParams` is the trait to implement to instruct how to go from `Option` json-rpc params 11 | //! to the handler's param types. 12 | //! - `IntoParams` has a default `into_params` implementation that will return an error if the params are missing. 13 | //! 14 | //! ```ignore // Example needs update for new types/macros 15 | //! #[derive(Deserialize)] 16 | //! pub struct ParamsIded { 17 | //! id: i64, 18 | //! } 19 | //! 20 | //! impl IntoParams for ParamsIded {} 21 | //! ``` 22 | //! 23 | //! - For custom `IntoParams` behavior, implement the `IntoParams::into_params` function. 24 | //! - Implementing `IntoDefaultParams` on a type that implements `Default` will auto-implement `IntoParams` 25 | //! and call `T::default()` when the params `Option` is None. 26 | //! 27 | 28 | // region: --- Modules 29 | 30 | mod support; 31 | 32 | mod error; 33 | mod handler; 34 | mod params; 35 | mod resource; 36 | mod router; 37 | mod rpc_id; 38 | mod rpc_message; 39 | mod rpc_response; // Added rpc_response module 40 | 41 | // -- Flatten 42 | pub use self::error::{Error, Result}; 43 | pub use handler::{Handler, HandlerError, HandlerResult, IntoHandlerError, RpcHandlerWrapperTrait}; 44 | pub use params::*; 45 | pub use resource::*; 46 | pub use router::*; 47 | pub use rpc_id::*; 48 | pub use rpc_message::*; 49 | pub use rpc_response::*; // Export rpc_response types 50 | 51 | // -- Export proc macros 52 | pub use rpc_router_macros::RpcHandlerError; 53 | pub use rpc_router_macros::RpcParams; 54 | pub use rpc_router_macros::RpcResource; 55 | 56 | // endregion: --- Modules 57 | -------------------------------------------------------------------------------- /examples/c03-with-derives.rs: -------------------------------------------------------------------------------- 1 | pub type Result = core::result::Result; 2 | pub type Error = Box; // For early dev. 3 | 4 | use rpc_router::{Handler, ResourcesBuilder, Router, RpcHandlerError, RpcParams, RpcResource}; 5 | // use serde::de::DeserializeOwned; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::json; 8 | use tokio::task::JoinSet; 9 | 10 | // region: --- Custom Error 11 | 12 | pub type MyResult = core::result::Result; 13 | 14 | #[derive(Debug, RpcHandlerError, Serialize)] 15 | pub enum MyError { 16 | // TBC 17 | } 18 | 19 | impl core::fmt::Display for MyError { 20 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { 21 | write!(fmt, "{self:?}") 22 | } 23 | } 24 | 25 | impl std::error::Error for MyError {} 26 | 27 | // endregion: --- Custom Error 28 | 29 | #[derive(Clone, RpcResource)] 30 | pub struct ModelManager {} 31 | 32 | #[derive(Deserialize, RpcParams)] 33 | pub struct ParamsIded { 34 | pub id: i64, 35 | } 36 | 37 | // impl IntoParams for ParamsForUpdate where D: DeserializeOwned + Send {} 38 | 39 | pub async fn get_task(_mm: ModelManager, params: ParamsIded) -> MyResult { 40 | Ok(params.id + 9000) 41 | } 42 | 43 | #[tokio::main] 44 | async fn main() -> Result<()> { 45 | // -- router & resources 46 | let rpc_router = Router::builder().append_dyn("get_task", get_task.into_dyn()).build(); 47 | 48 | // -- spawn calls 49 | let mut joinset = JoinSet::new(); 50 | for _ in 0..2 { 51 | let rpc_router = rpc_router.clone(); 52 | 53 | // In this example, we show the router might not have resources, and here we rebuild each time. 54 | let rpc_resources = ResourcesBuilder::default().append(ModelManager {}).build(); 55 | 56 | joinset.spawn(async move { 57 | let params = json!({"id": 123}); 58 | 59 | rpc_router 60 | .call_route_with_resources(None, "get_task", Some(params), rpc_resources) 61 | .await 62 | }); 63 | } 64 | 65 | // -- Print results 66 | while let Some(result) = joinset.join_next().await { 67 | println!("res: {result:?}"); 68 | } 69 | 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /src/params/into_params.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, Result}; 2 | use serde::de::DeserializeOwned; 3 | use serde_json::Value; 4 | 5 | /// `IntoParams` allows for converting an `Option` into 6 | /// the necessary type for RPC handler parameters. 7 | /// The default implementation below will result in failure if the value is `None`. 8 | /// For customized behavior, users can implement their own `into_params` 9 | /// method. 10 | pub trait IntoParams: DeserializeOwned + Send { 11 | fn into_params(value: Option) -> Result { 12 | match value { 13 | Some(value) => Ok(serde_json::from_value(value).map_err(Error::ParamsParsing)?), 14 | None => Err(Error::ParamsMissingButRequested), 15 | } 16 | } 17 | } 18 | 19 | /// Marker trait with a blanket implementation that return T::default 20 | /// if the `params: Option` is none. 21 | pub trait IntoDefaultRpcParams: DeserializeOwned + Send + Default {} 22 | 23 | impl

IntoParams for P 24 | where 25 | P: IntoDefaultRpcParams, 26 | { 27 | fn into_params(value: Option) -> Result { 28 | match value { 29 | Some(value) => Ok(serde_json::from_value(value).map_err(Error::ParamsParsing)?), 30 | None => Ok(Self::default()), 31 | } 32 | } 33 | } 34 | 35 | // region: --- Blanket implementation 36 | 37 | // IMPORTANT: Probably need to be put below a feature, like `with-blanket-option-params` 38 | 39 | /// Implements `IntoRpcParams` for any type that also implements `IntoRpcParams`. 40 | /// 41 | /// Note: Application code might prefer to avoid this blanket implementation. 42 | impl IntoParams for Option 43 | where 44 | D: DeserializeOwned + Send, 45 | D: IntoParams, 46 | { 47 | fn into_params(value: Option) -> Result { 48 | let value = value 49 | .map(|v| serde_json::from_value(v)) 50 | .transpose() 51 | .map_err(Error::ParamsParsing)?; 52 | Ok(value) 53 | } 54 | } 55 | 56 | // IMPORTANT: Probably need to be put below a feature, like `with-blanket-value-params` 57 | 58 | /// This is the IntoRpcParams implementation for serde_json Value. 59 | /// 60 | /// Note: As above, this might not be a capability app code might want to 61 | /// allow for rpc_handlers, prefering to have everything strongly type. 62 | impl IntoParams for Value {} 63 | 64 | // endregion: --- Blanket implementation 65 | -------------------------------------------------------------------------------- /src/rpc_message/rpc_request_parsing_error.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use serde_json::Value; 3 | use serde_with::{DisplayFromStr, serde_as}; 4 | 5 | /// The RPC Request Parsing error is used when utilizing `value.try_into()?` or `Request::from_value(value)`. 6 | /// The design intent is to validate and provide as much context as possible when a specific validation fails. 7 | /// 8 | /// Note: By design, we do not capture the "params" because they could be indefinitely large. 9 | /// 10 | /// Note: In future releases, the capture of Value objects or arrays for those error variants 11 | /// will be replaced with Value::String containing a message such as 12 | /// `"[object/array redacted, 'id' must be of type number, string, or equal to null]"` 13 | /// or `"[object/array redacted, 'method' must be of type string]"` 14 | /// This approach aims to provide sufficient context for debugging the issue while preventing 15 | /// the capture of indefinitely large values in the logs. 16 | #[serde_as] 17 | #[derive(Debug, Serialize)] 18 | pub enum RpcRequestParsingError { 19 | RequestInvalidType { 20 | actual_type: String, 21 | }, 22 | 23 | ParamsInvalidType { 24 | actual_type: String, 25 | }, 26 | 27 | VersionMissing { 28 | id: Option, // Keep Value here as RpcId parsing might not have happened yet 29 | method: Option, 30 | }, 31 | VersionInvalid { 32 | id: Option, // Keep Value here 33 | method: Option, 34 | version: Value, 35 | }, 36 | 37 | MethodMissing { 38 | id: Option, // Keep Value here 39 | }, 40 | MethodInvalidType { 41 | id: Option, // Keep Value here 42 | method: Value, 43 | }, 44 | 45 | NotificationHasId { 46 | method: Option, 47 | id: Value, 48 | }, 49 | 50 | MethodInvalid { 51 | actual: String, 52 | }, 53 | 54 | IdMissing { 55 | method: Option, 56 | }, 57 | IdInvalid { 58 | actual: String, 59 | cause: String, 60 | }, 61 | 62 | Parse(#[serde_as(as = "DisplayFromStr")] serde_json::Error), // Generic serde error if basic JSON is invalid 63 | } 64 | 65 | impl core::fmt::Display for RpcRequestParsingError { 66 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { 67 | write!(fmt, "{self:?}") 68 | } 69 | } 70 | 71 | impl std::error::Error for RpcRequestParsingError {} 72 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | `.` minor | `*` Major | `+` Addition | `^` improvement | `!` Change 2 | 3 | > For the `0.1.x` releases, there may be some changes to types or API naming. Therefore, the version should be locked to the latest version used, for example, `=0.1.0`. I will try to keep changes to a minimum, if any, and document them in the future [CHANGELOG](CHANGELOG.md). 4 | > 5 | > Once `0.2.0` is released, I will adhere more strictly to the semantic versioning methodology. 6 | 7 | ## 2024-03-14 - `0.1.3` 8 | 9 | - `+` add `RouterBuilder::extend_resources(..)` 10 | - `!` rename `RouterBuilder::set_resources_builder(..)` to `RouterBuilder::set_resources(..)` 11 | 12 | ## 2024-03-13 - `0.1.2` 13 | 14 | - `^` Add `IntoHandlerError` for `String`, `Value`, and `&'static str`. 15 | - `^` Add HandlerError::new:() 16 | - `.` remove `std::error::Error` error requirement for HandlerError .error 17 | 18 | ## 2024-03-12 - `0.1.1` 19 | 20 | > Note: `v0.1.1` changes from `0.1.0` 21 | > - `router.call(resources, request)` was **renamed** to `router.call_with_resources(request, resources)`. 22 | > - Now, the Router can have its own resources, enabling simpler and more efficient sharing of common resources across calls, 23 | > while still allowing custom resources to be overlaid at the call level. 24 | > - `router.call(request)` uses just the default caller resources. 25 | > 26 | > See [CHANGELOG](CHANGELOG.md) for more information. 27 | > 28 | > [Rust10x rust-web-app](https://github.com/rust10x/rust-web-app) has been updated. 29 | 30 | - `!` Changed `router.call(resources, request)` to `router.call_with_resources(request, resources)`. 31 | - `+` `Router` can now have base/common resources that are "injected" into every call. 32 | - Use `router_builder.append_resource(resource)...` to add resources. 33 | - The method `router.call_with_resources(request, resources)` overlays the call resources on top of the router resources. 34 | - `router.call(request)` uses only the Router resources. 35 | - `^` `router_builder!` macro now allows building the route and resource. 36 | 37 | ```rust 38 | let rpc_router = router_builder!( 39 | handlers: [get_task, create_task], // will be turned into routes 40 | resources: [ModelManager {}, AiManager {}] // common resources for all calls 41 | ) 42 | .build(); 43 | ``` 44 | 45 | ## 2024-03-11 - `0.1.0` 46 | 47 | - `*` Initial 48 | 49 | -------------------------------------------------------------------------------- /src/resource/resources.rs: -------------------------------------------------------------------------------- 1 | use super::resources_inner::ResourcesInner; 2 | use std::sync::Arc; 3 | 4 | // region: --- Builder 5 | 6 | #[derive(Debug, Default, Clone)] 7 | pub struct ResourcesBuilder { 8 | pub(crate) resources_inner: ResourcesInner, 9 | } 10 | 11 | impl ResourcesBuilder { 12 | pub fn get(&self) -> Option { 13 | self.resources_inner.get().cloned() 14 | } 15 | 16 | pub fn append(mut self, val: T) -> Self { 17 | self.resources_inner.insert(val); 18 | self 19 | } 20 | 21 | /// Convenient append method to avoid moving out value. 22 | /// Use `.append(val)` if not sure. 23 | pub fn append_mut(&mut self, val: T) { 24 | self.resources_inner.insert(val); 25 | } 26 | 27 | /// Build `Resources` with an `Arc` to enable efficient cloning without 28 | /// duplicating the content (i.e., without cloning the type hashmap). 29 | pub fn build(self) -> Resources { 30 | Resources::from_base_inner(self.resources_inner) 31 | } 32 | } 33 | 34 | // endregion: --- Builder 35 | 36 | // region: --- Resources 37 | 38 | #[derive(Debug, Clone, Default)] 39 | pub struct Resources { 40 | base_inner: Arc, 41 | overlay_inner: Arc, 42 | } 43 | 44 | // -- Builder 45 | impl Resources { 46 | /// Returns a new `ResourcesBuilder`. 47 | /// This is equivalent to calling `Resources::default()`. 48 | pub fn builder() -> ResourcesBuilder { 49 | ResourcesBuilder::default() 50 | } 51 | } 52 | 53 | // -- Public Methods 54 | impl Resources { 55 | pub fn get(&self) -> Option { 56 | // first additional, then base 57 | self.overlay_inner.get::().or_else(|| self.base_inner.get::()).cloned() 58 | } 59 | 60 | pub fn is_empty(&self) -> bool { 61 | self.base_inner.is_empty() && self.overlay_inner.is_empty() 62 | } 63 | } 64 | 65 | // -- Privates 66 | impl Resources { 67 | /// Build a resource from a base_inner ResourcesInner 68 | /// This is called bac the ResourcesBuilder 69 | pub(crate) fn from_base_inner(base_inner: ResourcesInner) -> Self { 70 | Self { 71 | base_inner: Arc::new(base_inner), 72 | overlay_inner: Default::default(), 73 | } 74 | } 75 | 76 | pub(crate) fn new_with_overlay(&self, overlay_resources: Resources) -> Self { 77 | Self { 78 | base_inner: self.base_inner.clone(), 79 | overlay_inner: overlay_resources.base_inner.clone(), 80 | } 81 | } 82 | } 83 | 84 | // endregion: --- Resources 85 | -------------------------------------------------------------------------------- /src/handler/handler_wrapper.rs: -------------------------------------------------------------------------------- 1 | use crate::Handler; 2 | use crate::handler::PinFutureValue; 3 | use crate::{Resources, Result}; 4 | use futures::Future; 5 | use serde_json::Value; 6 | use std::marker::PhantomData; 7 | use std::pin::Pin; 8 | 9 | /// `RpcHandlerWrapper` is an `RpcHandler` wrapper that implements 10 | /// `RpcHandlerWrapperTrait` for type erasure, enabling dynamic dispatch. 11 | /// Generics: 12 | /// - `H`: The handler trait for the function 13 | /// - `K`: The Resources, meaning the type passed in the call that has the `FromResources` trait for the various `T` types (cannot use `R`, as it is reserved for the 14 | /// - `T`: The type (can be a tuple when multiple) for the function parameters 15 | /// - `P`: The JSON RPC parameter 16 | /// - `R`: The response type 17 | /// 18 | /// Thus, all these types except `H` will match the generic of the `H` handler trait. We keep them in phantom data. 19 | #[derive(Clone)] 20 | pub struct RpcHandlerWrapper { 21 | handler: H, 22 | _marker: PhantomData<(T, P, R)>, 23 | } 24 | 25 | // Constructor 26 | impl RpcHandlerWrapper { 27 | pub fn new(handler: H) -> Self { 28 | Self { 29 | handler, 30 | _marker: PhantomData, 31 | } 32 | } 33 | } 34 | 35 | // Call Impl 36 | impl RpcHandlerWrapper 37 | where 38 | H: Handler + Send + Sync + 'static, 39 | T: Send + Sync + 'static, 40 | P: Send + Sync + 'static, 41 | R: Send + Sync + 'static, 42 | { 43 | pub fn call(&self, rpc_resources: Resources, params: Option) -> H::Future { 44 | // Note: Since handler is a FnOnce, we can use it only once, so we clone it. 45 | // This is likely optimized by the compiler. 46 | let handler = self.handler.clone(); 47 | Handler::call(handler, rpc_resources, params) 48 | } 49 | } 50 | 51 | /// `RpcHandlerWrapperTrait` enables `RpcHandlerWrapper` to become a trait object, 52 | /// allowing for dynamic dispatch. 53 | pub trait RpcHandlerWrapperTrait: Send + Sync { 54 | fn call(&self, rpc_resources: Resources, params: Option) -> PinFutureValue; 55 | } 56 | 57 | impl RpcHandlerWrapperTrait for RpcHandlerWrapper 58 | where 59 | H: Handler + Clone + Send + Sync + 'static, 60 | T: Send + Sync + 'static, 61 | P: Send + Sync + 'static, 62 | R: Send + Sync + 'static, 63 | { 64 | fn call( 65 | &self, 66 | rpc_resources: Resources, 67 | params: Option, 68 | ) -> Pin> + Send>> { 69 | Box::pin(self.call(rpc_resources, params)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/router/router_builder_macro.rs: -------------------------------------------------------------------------------- 1 | /// A simple macro to create a new RouterBuider from a list of handlers 2 | /// and optionaly a list of resources 3 | /// 4 | /// ## Pattern 1 - List of function handlers 5 | /// ``` 6 | /// router_builder!( 7 | /// create_project, 8 | /// list_projects, 9 | /// update_project, 10 | /// delete_project 11 | /// ); 12 | /// ``` 13 | /// Is equivalent to: 14 | /// ``` 15 | /// RouterBuilder::default() 16 | /// .append_dyn("create_project", create_project.into_box()) 17 | /// .append_dyn("list_projects", list_projects.into_box()) 18 | /// .append_dyn("update_project", update_project.into_box()) 19 | /// .append_dyn("delete_project", delete_project.into_box()) 20 | /// ``` 21 | /// 22 | /// ## Pattern 2 - List of function handlers, and resources 23 | /// ``` 24 | /// router_builder!( 25 | /// handlers: [get_task, create_task], // will be turned into routes 26 | /// resources: [ModelManager {}, AiManager {}] // common resources for all calls 27 | /// ); 28 | /// ``` 29 | /// 30 | /// Is equivalent to: 31 | /// 32 | /// ``` 33 | /// RouterBuilder::default() 34 | /// .append_dyn("get_task", get_task.into_box()) 35 | /// .append_dyn("create_task", create_task.into_box()) 36 | /// .append_resource(ModelManager {}) 37 | /// .append_resource(AiManager {}) 38 | /// ``` 39 | /// 40 | /// ## Pattern 3 - Just for consistency with Pattern 2, we can have omit the resources 41 | /// 42 | /// ``` 43 | /// router_builder!( 44 | /// handlers: [get_task, create_task] 45 | /// ); 46 | /// ``` 47 | /// 48 | #[macro_export] 49 | macro_rules! router_builder { 50 | // Pattern 1 - with `rpc_router!(my_fn1, myfn2)` 51 | ($($fn_name:ident),+ $(,)?) => { 52 | { 53 | use rpc_router::{Handler, RouterBuilder}; 54 | 55 | let mut builder = RouterBuilder::default(); 56 | $( 57 | builder = builder.append_dyn(stringify!($fn_name), $fn_name.into_dyn()); 58 | )+ 59 | builder 60 | } 61 | }; 62 | 63 | // Pattern 2 - `rpc_router!(handlers: [my_fn1, myfn2], resources: [ModelManger {}, AiManager {}])` 64 | (handlers: [$($handler:ident),* $(,)?], resources: [$($resource:expr),* $(,)?]) => {{ 65 | use rpc_router::{Handler, RouterBuilder}; 66 | 67 | let mut builder = RouterBuilder::default(); 68 | $( 69 | builder = builder.append_dyn(stringify!($handler), $handler.into_dyn()); 70 | )* 71 | $( 72 | builder = builder.append_resource($resource); 73 | )* 74 | builder 75 | }}; 76 | 77 | // Pattern 3 - with `rpc_router!(handlers: [my_fn1, myfn2])` 78 | (handlers: [$($handler:ident),* $(,)?]) => {{ 79 | use rpc_router::{Handler, RouterBuilder}; 80 | 81 | let mut builder = RouterBuilder::default(); 82 | $( 83 | builder = builder.append_dyn(stringify!($handler), $handler.into_dyn()); 84 | )* 85 | builder 86 | }}; 87 | } 88 | -------------------------------------------------------------------------------- /examples/c00-readme.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] // For examples. 2 | 3 | use rpc_router::{ 4 | CallSuccess, FromResources, IntoParams, RpcRequest, Resources, RpcHandlerError, RpcParams, RpcResource, 5 | resources_builder, router_builder, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | use serde_json::json; 9 | 10 | #[derive(Debug, derive_more::Display, RpcHandlerError)] 11 | pub enum MyError { 12 | // TBC 13 | #[display("TitleCannotBeEmpty")] 14 | TitleCannotBeEmpty, 15 | } 16 | 17 | // Make it a Resource with RpcResource derive macro 18 | #[derive(Clone, RpcResource)] 19 | pub struct ModelManager {} 20 | 21 | // Make it a Resource by implementing FromResources 22 | #[derive(Clone)] 23 | pub struct AiManager {} 24 | impl FromResources for AiManager {} 25 | 26 | // Make it a Params with RpcParams derive macro 27 | #[derive(Serialize, Deserialize, RpcParams)] 28 | pub struct TaskForCreate { 29 | title: String, 30 | done: Option, 31 | } 32 | 33 | // Make it a Params by implementing IntoParams 34 | #[derive(Deserialize)] 35 | pub struct ParamsIded { 36 | pub id: i64, 37 | } 38 | impl IntoParams for ParamsIded {} 39 | 40 | // Return values just need to implement Serialize 41 | #[derive(Serialize)] 42 | pub struct Task { 43 | id: i64, 44 | title: String, 45 | done: bool, 46 | } 47 | 48 | pub async fn get_task(mm: ModelManager, params: ParamsIded) -> Result { 49 | Ok(Task { 50 | id: params.id, 51 | title: "fake task".to_string(), 52 | done: false, 53 | }) 54 | } 55 | 56 | pub async fn create_task(mm: ModelManager, aim: AiManager, params: TaskForCreate) -> Result { 57 | Ok(123) // return fake id 58 | } 59 | 60 | #[tokio::main] 61 | async fn main() -> Result<(), Box> { 62 | // Build the Router with the handlers and common resources 63 | let rpc_router = router_builder!( 64 | handlers: [get_task, create_task], // will be turned into routes 65 | resources: [ModelManager {}, AiManager {}] // common resources for all calls 66 | ) 67 | .build(); 68 | // Can do the same with `Router::builder().append()/append_resource()` 69 | 70 | // Create and parse rpc request example. 71 | let rpc_request: RpcRequest = json!({ 72 | "jsonrpc": "2.0", 73 | "id": "some-client-req-id", // the json rpc id, that will get echoed back, can be null 74 | "method": "get_task", 75 | "params": { 76 | "id": 123 77 | } 78 | }) 79 | .try_into()?; 80 | 81 | // Async Execute the RPC Request with the router common resources 82 | let call_response = rpc_router.call(rpc_request).await?; 83 | 84 | // Or `call_with_resources` for additional per-call Resources that override router common resources. 85 | // e.g., rpc_router.call_with_resources(rpc_request, additional_resources) 86 | 87 | // Display the response. 88 | let CallSuccess { id, method, value } = call_response; 89 | println!( 90 | r#"RPC call response: 91 | 92 | id: {id:?}, 93 | method: {method}, 94 | value: {value:?}, 95 | "# 96 | ); 97 | 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /src/router/router_inner.rs: -------------------------------------------------------------------------------- 1 | use crate::handler::RpcHandlerWrapperTrait; 2 | use crate::{CallError, CallResult, CallSuccess, Error, Resources, RpcId, RpcRequest}; 3 | use serde_json::Value; 4 | use std::collections::HashMap; 5 | use std::fmt; 6 | 7 | /// method, which calls the appropriate handler matching the method_name. 8 | /// 9 | /// RouterInner can be extended with other RouterInners for composability. 10 | #[derive(Default)] 11 | pub(crate) struct RouterInner { 12 | route_by_name: HashMap<&'static str, Box>, 13 | } 14 | 15 | impl fmt::Debug for RouterInner { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | f.debug_struct("RouterInner") 18 | .field("route_by_name", &self.route_by_name.keys()) 19 | .finish() 20 | } 21 | } 22 | 23 | impl RouterInner { 24 | /// Add a dyn_handler to the router. 25 | /// 26 | /// ``` 27 | /// RouterInner::new().add_dyn("method_name", my_handler_fn.into_dyn()); 28 | /// ``` 29 | /// 30 | /// Note: This is the preferred way to add handlers to the router, as it 31 | /// avoids monomorphization of the add function. 32 | /// The RouterInner also has a `.add()` as a convenience function to just pass the function. 33 | /// See `RouterInner::add` for more details. 34 | pub fn append_dyn(&mut self, name: &'static str, dyn_handler: Box) { 35 | self.route_by_name.insert(name, dyn_handler); 36 | } 37 | 38 | pub fn extend(&mut self, other_router: RouterInner) { 39 | self.route_by_name.extend(other_router.route_by_name); 40 | } 41 | 42 | /// Performs the RPC call for a given Request object, which contains the `id`, method name, and parameters. 43 | /// 44 | /// Returns an ResponseResult, where either the success value (Response) or the error (ResponseError) 45 | /// will echo back the `id` and `method` part of their construct 46 | pub async fn call(&self, resources: Resources, rpc_request: RpcRequest) -> CallResult { 47 | let RpcRequest { id, method, params } = rpc_request; 48 | 49 | self.call_route(resources, id, method, params).await 50 | } 51 | 52 | /// Performs the RPC call given the id, method, and params. 53 | /// 54 | /// - method: The json-rpc method name. 55 | /// - id: The json-rpc request ID. If None, defaults to RpcId::Null. 56 | /// - params: The optional json-rpc params. 57 | /// 58 | /// Returns a CallResult, where either the success value (CallSuccess) or the error (CallError) 59 | /// will include the original `id` and `method`. 60 | pub async fn call_route( 61 | &self, 62 | resources: Resources, 63 | id: RpcId, 64 | method: impl Into, 65 | params: Option, 66 | ) -> CallResult { 67 | let method = method.into(); 68 | 69 | if let Some(route) = self.route_by_name.get(method.as_str()) { 70 | match route.call(resources, params).await { 71 | Ok(value) => Ok(CallSuccess { 72 | id: id.clone(), // Clone id for the response 73 | method: method.clone(), 74 | value, 75 | }), 76 | Err(error) => Err(CallError { id, method, error }), 77 | } 78 | } else { 79 | Err(CallError { 80 | id, 81 | method, 82 | error: Error::MethodUnknown, 83 | }) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/router/router_builder.rs: -------------------------------------------------------------------------------- 1 | use crate::handler::RpcHandlerWrapperTrait; 2 | use crate::router::router_inner::RouterInner; 3 | use crate::{FromResources, Handler, ResourcesBuilder, ResourcesInner, Router}; 4 | 5 | #[derive(Debug, Default)] 6 | pub struct RouterBuilder { 7 | inner: RouterInner, 8 | base_resources_inner: ResourcesInner, 9 | } 10 | 11 | impl RouterBuilder { 12 | /// Add a dyn_handler to the router builder. 13 | /// 14 | /// ``` 15 | /// RouterBuilder::default().add_dyn("method_name", my_handler_fn.into_dyn()); 16 | /// ``` 17 | /// 18 | /// Note: This is the preferred way to add handlers to the router, as it 19 | /// avoids monomorphization of the add function. 20 | /// The `RouterInner` also has a `.add()` as a convenience function to just pass the function. 21 | /// See `RouterInner::add` for more details. 22 | pub fn append_dyn(mut self, name: &'static str, dyn_handler: Box) -> Self { 23 | self.inner.append_dyn(name, dyn_handler); 24 | self 25 | } 26 | 27 | /// Add a route (name, handler function) to the builder 28 | /// 29 | /// ``` 30 | /// RouterBuilder::default().add("method_name", my_handler_fn); 31 | /// ``` 32 | /// 33 | /// Note: This is a convenient add function variant with generics, 34 | /// and there will be monomorphed versions of this function 35 | /// for each type passed. Use `RouterInner::add_dyn` to avoid this. 36 | pub fn append(mut self, name: &'static str, handler: F) -> Self 37 | where 38 | F: Handler + Clone + Send + Sync + 'static, 39 | T: Send + Sync + 'static, 40 | P: Send + Sync + 'static, 41 | R: Send + Sync + 'static, 42 | { 43 | self.inner.append_dyn(name, handler.into_dyn()); 44 | self 45 | } 46 | 47 | /// Extends this builder by consuming another builder. 48 | pub fn extend(mut self, other_builder: RouterBuilder) -> Self { 49 | self.inner.extend(other_builder.inner); 50 | self.base_resources_inner.extend(other_builder.base_resources_inner); 51 | self 52 | } 53 | 54 | pub fn append_resource(mut self, val: T) -> Self 55 | where 56 | T: FromResources + Clone + Send + Sync + 'static, 57 | { 58 | self.base_resources_inner.insert(val); 59 | self 60 | } 61 | 62 | /// If Some, will extends the current Base Resources with the content of this resources_builder. 63 | /// If None, just do nothing. 64 | pub fn extend_resources(mut self, resources_builder: Option) -> Self { 65 | if let Some(resources_builder) = resources_builder { 66 | // if self resources empty, no need to extend 67 | if self.base_resources_inner.is_empty() { 68 | self.base_resources_inner = resources_builder.resources_inner 69 | } 70 | // if not empty, we extend 71 | else { 72 | self.base_resources_inner.extend(resources_builder.resources_inner) 73 | } 74 | } 75 | self 76 | } 77 | 78 | /// Resets the router's resources with the contents of this ResourcesBuilder. 79 | /// 80 | /// Ensure to call `append_resource` and/or `.extend_resources` afterwards 81 | /// if they operation needs to be included. 82 | /// 83 | /// Note: `.extend_resources(Option)` is the additive function 84 | /// typically used. 85 | pub fn set_resources(mut self, resources_builder: ResourcesBuilder) -> Self { 86 | self.base_resources_inner = resources_builder.resources_inner; 87 | self 88 | } 89 | 90 | /// Builds the `RpcRouter` from this builder. 91 | /// This is the typical usage, with the `RpcRouter` being encapsulated in an `Arc`, 92 | /// indicating it is designed for cloning and sharing across tasks/threads. 93 | pub fn build(self) -> Router { 94 | Router::new(self.inner, self.base_resources_inner) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/handler/handler_error.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Serializer}; 2 | use serde_json::Value; 3 | use std::any::{Any, TypeId}; 4 | use std::collections::HashMap; 5 | 6 | pub type HandlerResult = core::result::Result; 7 | 8 | type AnyMap = HashMap>; 9 | 10 | #[derive(Debug)] 11 | pub struct HandlerError { 12 | holder: AnyMap, 13 | type_name: &'static str, 14 | } 15 | 16 | impl HandlerError { 17 | pub fn new(val: T) -> HandlerError 18 | where 19 | T: Any + Send + Sync, 20 | { 21 | let mut holder = AnyMap::with_capacity(1); 22 | let type_name = std::any::type_name::(); 23 | holder.insert(TypeId::of::(), Box::new(val)); 24 | HandlerError { holder, type_name } 25 | } 26 | } 27 | 28 | impl HandlerError { 29 | /// Returns an option containing a reference if the error contained within this error 30 | /// matches the requested type. 31 | pub fn get(&self) -> Option<&T> { 32 | self.holder 33 | .get(&TypeId::of::()) 34 | .and_then(|boxed_any| boxed_any.downcast_ref::()) 35 | } 36 | 37 | /// Same as `get::()` but remove the date so that it returns a owned value. 38 | pub fn remove(&mut self) -> Option { 39 | self.holder.remove(&TypeId::of::()).and_then(|boxed_any| { 40 | // Attempt to downcast the Box into Box. If successful, take the value out of the box. 41 | (boxed_any as Box).downcast::().ok().map(|boxed| *boxed) 42 | }) 43 | } 44 | 45 | /// Return the type name of the error hold by this RpcHandlerError 46 | pub fn type_name(&self) -> &'static str { 47 | self.type_name 48 | } 49 | } 50 | 51 | // Implementing Serialize for RpcHandlerError 52 | impl Serialize for HandlerError { 53 | fn serialize(&self, serializer: S) -> Result 54 | where 55 | S: Serializer, 56 | { 57 | // By default, serialization will only serialize an informative message regarding the type of error contained, 58 | // as we do not have more information at this point. 59 | // NOTE: It is currently uncertain whether we should require serialization for the RpcHandlerError contained type. 60 | serializer.serialize_str(&format!("RpcHandlerError containing error '{}'", self.type_name)) 61 | } 62 | } 63 | 64 | // region: --- IntoRpcHandlerError 65 | 66 | /// A trait with a default implementation that converts any application error 67 | /// into a `RpcHandlerError`. This allows the application code 68 | /// to query and extract the specified application error. 69 | pub trait IntoHandlerError 70 | where 71 | Self: Sized + Send + Sync + 'static, 72 | { 73 | fn into_handler_error(self) -> HandlerError { 74 | HandlerError::new(self) 75 | } 76 | } 77 | 78 | impl IntoHandlerError for HandlerError { 79 | fn into_handler_error(self) -> HandlerError { 80 | self 81 | } 82 | } 83 | 84 | impl IntoHandlerError for String { 85 | fn into_handler_error(self) -> HandlerError { 86 | HandlerError::new(self) 87 | } 88 | } 89 | 90 | impl IntoHandlerError for &'static str { 91 | fn into_handler_error(self) -> HandlerError { 92 | HandlerError::new(self) 93 | } 94 | } 95 | 96 | impl IntoHandlerError for Value { 97 | fn into_handler_error(self) -> HandlerError { 98 | HandlerError::new(self) 99 | } 100 | } 101 | 102 | // endregion: --- IntoRpcHandlerError 103 | 104 | // region: --- Error Boilerplate 105 | 106 | impl core::fmt::Display for HandlerError { 107 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { 108 | write!(fmt, "{self:?}") 109 | } 110 | } 111 | 112 | impl std::error::Error for HandlerError {} 113 | 114 | // endregion: --- Error Boilerplate 115 | -------------------------------------------------------------------------------- /tests/test-calls.rs: -------------------------------------------------------------------------------- 1 | pub type Result = core::result::Result; 2 | pub type Error = Box; // For early dev. 3 | 4 | use rpc_router::{FromResources, Handler, HandlerResult, IntoParams, RpcRequest, Resources, Router, RpcId}; 5 | use serde::Deserialize; 6 | use serde_json::json; 7 | use tokio::task::JoinSet; 8 | 9 | // region: --- Test Assets 10 | 11 | #[derive(Clone)] 12 | pub struct ModelManager; 13 | impl FromResources for ModelManager {} 14 | 15 | #[derive(Deserialize)] 16 | pub struct ParamsIded { 17 | pub id: i64, 18 | } 19 | impl IntoParams for ParamsIded {} 20 | 21 | pub async fn get_task(_mm: ModelManager, params: ParamsIded) -> HandlerResult { 22 | Ok(params.id + 9000) 23 | } 24 | 25 | // endregion: --- Test Assets 26 | 27 | #[tokio::test] 28 | async fn test_sync_call() -> Result<()> { 29 | // -- Setup & Fixtures 30 | let fx_num = 123; 31 | let fx_res_value = 9123; 32 | let rpc_router = Router::builder() 33 | .append_dyn("get_task", get_task.into_dyn()) 34 | .append_resource(ModelManager) 35 | .build(); 36 | 37 | let rpc_request: RpcRequest = json!({ 38 | "jsonrpc": "2.0", 39 | "id": null, // the json rpc id, that will get echoed back, can be null 40 | "method": "get_task", 41 | "params": { 42 | "id": fx_num 43 | } 44 | }) 45 | .try_into()?; 46 | 47 | // -- Exec 48 | let res = rpc_router.call(rpc_request).await?; 49 | 50 | // -- Check 51 | let res_value: i32 = serde_json::from_value(res.value)?; 52 | assert_eq!(res_value, fx_res_value); 53 | 54 | Ok(()) 55 | } 56 | 57 | #[tokio::test] 58 | async fn test_async_calls() -> Result<()> { 59 | // -- Setup & Fixtures 60 | let fx_num = 124; 61 | let fx_res_value = 9124; 62 | let rpc_router = Router::builder().append_dyn("get_task", get_task.into_dyn()).build(); 63 | 64 | // -- spawn calls 65 | let mut joinset = JoinSet::new(); 66 | for idx in 0..2 { 67 | let rpc_router = rpc_router.clone(); 68 | let rpc_resources = Resources::builder().append(ModelManager).build(); 69 | let rpc_request: RpcRequest = json!({ 70 | "jsonrpc": "2.0", 71 | "id": idx, // the json rpc id, that will get echoed back, can be null 72 | "method": "get_task", 73 | "params": { 74 | "id": fx_num 75 | } 76 | }) 77 | .try_into()?; 78 | 79 | joinset.spawn(async move { 80 | let rpc_router = rpc_router.clone(); 81 | 82 | rpc_router.call_with_resources(rpc_request, rpc_resources).await 83 | }); 84 | } 85 | 86 | // -- Check 87 | let mut fx_rpc_id_num = 0; 88 | while let Some(res) = joinset.join_next().await { 89 | let rpc_response = res??; 90 | 91 | // check rpc_id 92 | let fx_rpc_id = RpcId::from_value(json!(fx_rpc_id_num))?; 93 | assert_eq!(rpc_response.id, fx_rpc_id); 94 | fx_rpc_id_num += 1; 95 | 96 | // check result value 97 | let res = rpc_response.value; 98 | let res_value: i32 = serde_json::from_value(res)?; 99 | assert_eq!(res_value, fx_res_value); 100 | } 101 | 102 | Ok(()) 103 | } 104 | 105 | #[tokio::test] 106 | async fn test_shared_resources() -> Result<()> { 107 | // -- Setup & Fixtures 108 | let fx_num = 125; 109 | let fx_res_value = 9125; 110 | let rpc_router = Router::builder().append_dyn("get_task", get_task.into_dyn()).build(); 111 | let rpc_resources = Resources::builder().append(ModelManager).build(); 112 | 113 | // -- spawn calls 114 | let mut joinset = JoinSet::new(); 115 | for _ in 0..2 { 116 | let rpc_router = rpc_router.clone(); 117 | let rpc_resources = rpc_resources.clone(); 118 | joinset.spawn(async move { 119 | let rpc_router = rpc_router.clone(); 120 | 121 | let params = json!({"id": fx_num}); 122 | 123 | rpc_router 124 | .call_route_with_resources(None, "get_task", Some(params), rpc_resources) 125 | .await 126 | }); 127 | } 128 | 129 | // -- Check 130 | while let Some(res) = joinset.join_next().await { 131 | let res = res??; 132 | let res_value: i32 = serde_json::from_value(res.value)?; 133 | assert_eq!(res_value, fx_res_value); 134 | } 135 | 136 | Ok(()) 137 | } 138 | -------------------------------------------------------------------------------- /src/handler/impl_handlers.rs: -------------------------------------------------------------------------------- 1 | use crate::Resources; 2 | 3 | /// Macro generatring the Rpc Handler implementations for zero or more FromResources with the last argument being IntoParams 4 | /// and one with not last IntoParams argument. 5 | #[macro_export] 6 | macro_rules! impl_handler_pair { 7 | ($K:ty, $($T:ident),*) => { 8 | 9 | // Handler implementations for zero or more FromResources with the last argument being IntoParams 10 | impl $crate::Handler<($($T,)*), (P,), R> for F 11 | where 12 | F: FnOnce($($T,)* P) -> Fut + Clone + Send + 'static, 13 | $( $T: $crate::FromResources+ Clone + Send + Sync + 'static, )* 14 | P: $crate::IntoParams + Send + Sync + 'static, 15 | R: serde::Serialize + Send + Sync + 'static, 16 | E: $crate::IntoHandlerError, 17 | Fut: futures::Future> + Send, 18 | { 19 | type Future = $crate::handler::PinFutureValue; 20 | 21 | #[allow(unused)] // somehow resources will be marked as unused 22 | fn call( 23 | self, 24 | resources: Resources, 25 | params_value: Option, 26 | ) -> Self::Future { 27 | Box::pin(async move { 28 | let param = P::into_params(params_value)?; 29 | 30 | let res = self( 31 | $( $T::from_resources(&resources)?, )* 32 | param, 33 | ).await; 34 | 35 | match res { 36 | Ok(result) => Ok(serde_json::to_value(result).map_err($crate::Error::HandlerResultSerialize)?), 37 | Err(ex) => { 38 | let he = $crate::IntoHandlerError::into_handler_error(ex); 39 | Err(he.into()) 40 | }, 41 | } 42 | }) 43 | } 44 | } 45 | 46 | // Handler implementations for zero or more FromResources and NO IntoParams 47 | impl $crate::Handler<($($T,)*), (), R> for F 48 | where 49 | F: FnOnce($($T,)*) -> Fut + Clone + Send + 'static, 50 | $( $T: $crate::FromResources + Clone + Send + Sync + 'static, )* 51 | R: serde::Serialize + Send + Sync + 'static, 52 | E: $crate::IntoHandlerError, 53 | Fut: futures::Future> + Send, 54 | { 55 | type Future = $crate::handler::PinFutureValue; 56 | 57 | #[allow(unused)] // somehow resources will be marked as unused 58 | fn call( 59 | self, 60 | resources: Resources, 61 | _params: Option, 62 | ) -> Self::Future { 63 | Box::pin(async move { 64 | let res = self( 65 | $( $T::from_resources(&resources)?, )* 66 | ).await; 67 | 68 | match res { 69 | Ok(result) => Ok(serde_json::to_value(result).map_err($crate::Error::HandlerResultSerialize)?), 70 | Err(ex) => { 71 | let he = $crate::IntoHandlerError::into_handler_error(ex); 72 | Err(he.into()) 73 | }, 74 | } 75 | 76 | }) 77 | } 78 | } 79 | }; 80 | 81 | } 82 | 83 | impl_handler_pair!(Resources,); 84 | impl_handler_pair!(Resources, T1); 85 | impl_handler_pair!(Resources, T1, T2); 86 | impl_handler_pair!(Resources, T1, T2, T3); 87 | impl_handler_pair!(Resources, T1, T2, T3, T4); 88 | impl_handler_pair!(Resources, T1, T2, T3, T4, T5); 89 | impl_handler_pair!(Resources, T1, T2, T3, T4, T5, T6); 90 | impl_handler_pair!(Resources, T1, T2, T3, T4, T5, T6, T7); 91 | impl_handler_pair!(Resources, T1, T2, T3, T4, T5, T6, T7, T8); 92 | -------------------------------------------------------------------------------- /src/rpc_response/rpc_error.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::{Value, json}; 4 | 5 | /// Represents the JSON-RPC 2.0 Error Object. 6 | /// 7 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 8 | pub struct RpcError { 9 | /// A Number that indicates the error type that occurred. 10 | pub code: i64, 11 | 12 | /// A String providing a short description of the error. 13 | pub message: String, 14 | 15 | /// A Primitive or Structured value that contains additional information about the error. 16 | /// This may be omitted. 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | pub data: Option, 19 | } 20 | 21 | // region: --- Predefined Errors 22 | // https://www.jsonrpc.org/specification#error_object 23 | impl RpcError { 24 | pub const CODE_PARSE_ERROR: i64 = -32700; 25 | pub const CODE_INVALID_REQUEST: i64 = -32600; 26 | pub const CODE_METHOD_NOT_FOUND: i64 = -32601; 27 | pub const CODE_INVALID_PARAMS: i64 = -32602; 28 | pub const CODE_INTERNAL_ERROR: i64 = -32603; 29 | // -32000 to -32099: Server error. Reserved for implementation-defined server-errors. 30 | 31 | pub fn from_parse_error(data: Option) -> Self { 32 | Self { 33 | code: Self::CODE_PARSE_ERROR, 34 | message: "Parse error".to_string(), 35 | data, 36 | } 37 | } 38 | 39 | pub fn from_invalid_request(data: Option) -> Self { 40 | Self { 41 | code: Self::CODE_INVALID_REQUEST, 42 | message: "Invalid Request".to_string(), 43 | data, 44 | } 45 | } 46 | 47 | pub fn from_method_not_found(data: Option) -> Self { 48 | Self { 49 | code: Self::CODE_METHOD_NOT_FOUND, 50 | message: "Method not found".to_string(), 51 | data, 52 | } 53 | } 54 | 55 | pub fn from_invalid_params(data: Option) -> Self { 56 | Self { 57 | code: Self::CODE_INVALID_PARAMS, 58 | message: "Invalid params".to_string(), 59 | data, 60 | } 61 | } 62 | 63 | pub fn from_internal_error(data: Option) -> Self { 64 | Self { 65 | code: Self::CODE_INTERNAL_ERROR, 66 | message: "Internal error".to_string(), 67 | data, 68 | } 69 | } 70 | 71 | /// Helper to create an RpcError with optional data representing the original error string. 72 | fn new(code: i64, message: impl Into, error: Option<&dyn std::error::Error>) -> Self { 73 | let data = error.map(|e| json!(e.to_string())); 74 | Self { 75 | code, 76 | message: message.into(), 77 | data, 78 | } 79 | } 80 | } 81 | // endregion: --- Predefined Errors 82 | 83 | // region: --- From RouterError 84 | 85 | impl From<&Error> for RpcError { 86 | /// Converts a router `Error` into a JSON-RPC `RpcError`. 87 | fn from(err: &Error) -> Self { 88 | match err { 89 | Error::ParamsParsing(p) => Self::new(Self::CODE_INVALID_PARAMS, "Invalid params", Some(p)), 90 | Error::ParamsMissingButRequested => Self::new(Self::CODE_INVALID_PARAMS, "Invalid params", Some(err)), 91 | Error::MethodUnknown => Self::new(Self::CODE_METHOD_NOT_FOUND, "Method not found", Some(err)), 92 | Error::FromResources(fr_err) => Self::new(Self::CODE_INTERNAL_ERROR, "Internal error", Some(fr_err)), 93 | Error::HandlerResultSerialize(s_err) => Self::new(Self::CODE_INTERNAL_ERROR, "Internal error", Some(s_err)), 94 | // NOTE: For HandlerError, we use a generic Internal Error. 95 | // A future enhancement could involve a trait on the error 96 | // wrapped by HandlerError to provide specific RpcError details. 97 | Error::Handler(h_err) => Self::new(Self::CODE_INTERNAL_ERROR, "Internal error", Some(h_err)), 98 | } 99 | } 100 | } 101 | 102 | // endregion: --- From RouterError 103 | 104 | // region: --- From CallError 105 | 106 | // We also implement From for RpcError for convenience, although 107 | // the direct conversion from CallError to RpcResponse is often more useful. 108 | impl From for RpcError { 109 | fn from(call_error: crate::CallError) -> Self { 110 | // Reuse the logic from From<&RouterError> 111 | RpcError::from(&call_error.error) 112 | } 113 | } 114 | 115 | impl From<&crate::CallError> for RpcError { 116 | fn from(call_error: &crate::CallError) -> Self { 117 | // Reuse the logic from From<&RouterError> 118 | RpcError::from(&call_error.error) 119 | } 120 | } 121 | 122 | // endregion: --- From CallError 123 | -------------------------------------------------------------------------------- /src/router/router.rs: -------------------------------------------------------------------------------- 1 | use crate::router::router_inner::RouterInner; 2 | use crate::{CallResult, ResourcesInner, RouterBuilder, RpcRequest}; 3 | use crate::{FromResources, Resources, RpcId}; 4 | use serde_json::Value; 5 | use std::sync::Arc; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct Router { 9 | inner: Arc, 10 | base_resources: Resources, 11 | } 12 | 13 | //-- Builder 14 | impl Router { 15 | /// Returns a new `RouterBuilder`. 16 | /// This is equivalent to calling `Router::builder()`. 17 | pub fn builder() -> RouterBuilder { 18 | RouterBuilder::default() 19 | } 20 | } 21 | impl FromResources for Router {} 22 | 23 | // -- Methods 24 | impl Router { 25 | /// Performs the RPC call for a given RpcRequest object (i.e., `.id, .method, .params`) 26 | /// with the eventual resources of the router. 27 | /// 28 | /// To add additional resources on top of the router's resources, call `.call_with_resources(request, resources)` 29 | /// 30 | /// - Returns an CallResult that echoes the `id` and `method`, and includes the `Result` result. 31 | /// 32 | /// - The `rpc_router::Error` includes a variant `rpc_router::Error::Handler(HandlerError)`, 33 | /// where `HandlerError` allows retrieval of the application error returned by the handler 34 | /// through `HandlerError::get::(&self) -> Option`. 35 | /// This mechanism enables application RPC handlers to return specific application errors while still utilizing 36 | /// the `rpc-router` result structure, thereby allowing them to retrieve their specific error type. 37 | /// 38 | pub async fn call(&self, rpc_request: RpcRequest) -> CallResult { 39 | self.inner.call(self.base_resources.clone(), rpc_request).await 40 | } 41 | 42 | /// Similar to `.call(...)`, but takes an additional `Resources` parameter that will be overlaid on top 43 | /// of the eventual base router resources. 44 | /// 45 | /// Note: The router will first try to get the resource from the overlay, and then, 46 | /// will try the base router resources. 47 | pub async fn call_with_resources(&self, rpc_request: RpcRequest, additional_resources: Resources) -> CallResult { 48 | let resources = self.compute_call_resources(additional_resources); 49 | 50 | self.inner.call(resources, rpc_request).await 51 | } 52 | 53 | /// Lower level function to `.call` which take all Rpc Request properties as value. 54 | /// If id is None, it will be set a Value::Null 55 | /// 56 | /// This also use router base resources. 57 | /// 58 | /// To add additional resources on top of the router's resources, call `.call_route_with_resources(request, resources)` 59 | /// 60 | /// - method: The json-rpc method name. 61 | /// - id: The json-rpc request ID. If None, defaults to RpcId::Null. 62 | /// - params: The optional json-rpc params 63 | /// 64 | /// Returns an CallResult, where either the success value (CallSuccess) or the error (CallError) 65 | /// will echo back the `id` and `method` part of their construct 66 | pub async fn call_route(&self, id: Option, method: impl Into, params: Option) -> CallResult { 67 | let id = id.unwrap_or_default(); // Default to RpcId::Null if None 68 | self.inner.call_route(self.base_resources.clone(), id, method, params).await 69 | } 70 | 71 | /// Similar to `.call_route`, but takes an additional `Resources` parameter that will be overlaid on top 72 | /// of the eventual base router resources. 73 | /// 74 | /// Note: The router will first try to get the resource from the overlay, and then, 75 | /// will try the base router resources. 76 | pub async fn call_route_with_resources( 77 | &self, 78 | id: Option, 79 | method: impl Into, 80 | params: Option, 81 | additional_resources: Resources, 82 | ) -> CallResult { 83 | let resources = self.compute_call_resources(additional_resources); 84 | let id = id.unwrap_or_default(); // Default to RpcId::Null if None 85 | 86 | self.inner.call_route(resources, id, method, params).await 87 | } 88 | } 89 | 90 | // Crate only method 91 | impl Router { 92 | /// For specific or advanced use cases. 93 | /// 94 | /// Use `RouterBuilder::default()...build()` if unsure. 95 | /// 96 | /// Creates an `Router` from its inner data. 97 | /// 98 | /// Note: This is intended for situations where a custom builder 99 | /// workflow is needed. The recommended method for creating an `Router` 100 | /// is via the `RouterBuilder`. 101 | pub(crate) fn new(inner: RouterInner, resources_inner: ResourcesInner) -> Self { 102 | Self { 103 | inner: Arc::new(inner), 104 | base_resources: Resources::from_base_inner(resources_inner), 105 | } 106 | } 107 | 108 | pub(crate) fn compute_call_resources(&self, call_resources: Resources) -> Resources { 109 | if self.base_resources.is_empty() { 110 | call_resources 111 | } else { 112 | self.base_resources.new_with_overlay(call_resources) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/test-custom-error.rs: -------------------------------------------------------------------------------- 1 | use rpc_router::{FromResources, Handler, IntoHandlerError, IntoParams, Resources, Router, router_builder}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::json; 4 | use tokio::task::JoinSet; 5 | 6 | // region: --- Custom Error 7 | 8 | #[derive(Debug, Serialize)] 9 | pub enum MyError { 10 | IdTooBig, 11 | AnotherVariant(String), 12 | } 13 | impl IntoHandlerError for MyError {} 14 | 15 | impl core::fmt::Display for MyError { 16 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { 17 | write!(fmt, "{self:?}") 18 | } 19 | } 20 | 21 | impl std::error::Error for MyError {} 22 | 23 | // endregion: --- Custom Error 24 | 25 | // region: --- Test Assets 26 | 27 | #[derive(Clone)] 28 | pub struct ModelManager; 29 | impl FromResources for ModelManager {} 30 | 31 | #[derive(Deserialize)] 32 | pub struct ParamsIded { 33 | pub id: i64, 34 | } 35 | impl IntoParams for ParamsIded {} 36 | 37 | pub async fn get_task(_mm: ModelManager, params: ParamsIded) -> Result { 38 | if params.id > 200 { 39 | Err(MyError::IdTooBig) 40 | } else { 41 | Ok(params.id + 5000) 42 | } 43 | } 44 | 45 | pub async fn get_count(_mm: ModelManager, _params: ParamsIded) -> Result { 46 | Err("Always a String error".to_string()) 47 | } 48 | 49 | pub async fn get_count_str(_mm: ModelManager, _params: ParamsIded) -> Result { 50 | Err("Always a str error") 51 | } 52 | 53 | // endregion: --- Test Assets 54 | 55 | #[tokio::test] 56 | async fn test_custom_my_error() -> Result<(), Box> { 57 | // -- Setup & Fixtures 58 | let fx_nums = [123, 222]; 59 | let fx_res_values = [5123, -1]; 60 | let rpc_router = Router::builder().append_dyn("get_task", get_task.into_dyn()).build(); 61 | let rpc_resources = Resources::builder().append(ModelManager).build(); 62 | 63 | // -- spawn calls 64 | let mut joinset = JoinSet::new(); 65 | for (idx, fx_num) in fx_nums.into_iter().enumerate() { 66 | let rpc_router = rpc_router.clone(); 67 | let rpc_resources = rpc_resources.clone(); 68 | joinset.spawn(async move { 69 | let rpc_router = rpc_router.clone(); 70 | 71 | // Cheap way to "ensure" start spawns matches join_next order. (not for prod) 72 | tokio::time::sleep(std::time::Duration::from_millis(idx as u64 * 10)).await; 73 | 74 | let params = json!({"id": fx_num}); 75 | 76 | rpc_router 77 | .call_route_with_resources(None, "get_task", Some(params), rpc_resources) 78 | .await 79 | }); 80 | } 81 | 82 | // -- Check 83 | // first, should be 5123 84 | let res = joinset.join_next().await.ok_or("missing first result")???; 85 | let res_value: i32 = serde_json::from_value(res.value)?; 86 | assert_eq!(fx_res_values[0], res_value); 87 | 88 | // second, should be the IdToBig error 89 | if let Err(err) = joinset.join_next().await.ok_or("missing second result")?? { 90 | match err.error { 91 | rpc_router::Error::Handler(handler_error) => { 92 | if let Some(my_error) = handler_error.get::() { 93 | assert!( 94 | matches!(my_error, MyError::IdTooBig), 95 | "should have matched MyError::IdTooBig" 96 | ) 97 | } else { 98 | let type_name = handler_error.type_name(); 99 | return Err( 100 | format!("HandlerError should be holding a MyError, but was holding {type_name}") 101 | .to_string() 102 | .into(), 103 | ); 104 | } 105 | } 106 | _other => return Err("second result should be a rpc_router::Error:Handler".to_string().into()), 107 | } 108 | } else { 109 | return Err("second set should have returned error".into()); 110 | } 111 | 112 | Ok(()) 113 | } 114 | 115 | #[tokio::test] 116 | async fn test_custom_string_error() -> Result<(), Box> { 117 | let rpc_router = router_builder![ 118 | handlers: [get_task, get_count], 119 | resources: [ModelManager] 120 | ] 121 | .build(); 122 | 123 | let Err(call_err) = rpc_router.call_route(None, "get_count", Some(json! ({"id": 123}))).await else { 124 | return Err("Should have returned Error".into()); 125 | }; 126 | 127 | let rpc_router::Error::Handler(mut handler_error) = call_err.error else { 128 | return Err("Should have returned a HandlerError".into()); 129 | }; 130 | 131 | assert_eq!( 132 | handler_error.remove::(), 133 | Some("Always a String error".to_string()) 134 | ); 135 | 136 | Ok(()) 137 | } 138 | 139 | #[tokio::test] 140 | async fn test_custom_str_error() -> Result<(), Box> { 141 | let rpc_router = router_builder![ 142 | handlers: [get_task, get_count, get_count_str], 143 | resources: [ModelManager] 144 | ] 145 | .build(); 146 | 147 | let Err(call_err) = rpc_router.call_route(None, "get_count_str", Some(json! ({"id": 123}))).await else { 148 | return Err("Should have returned Error".into()); 149 | }; 150 | 151 | let rpc_router::Error::Handler(mut handler_error) = call_err.error else { 152 | return Err("Should have returned a HandlerError".into()); 153 | }; 154 | 155 | assert_eq!(handler_error.remove::<&'static str>(), Some("Always a str error")); 156 | 157 | Ok(()) 158 | } 159 | -------------------------------------------------------------------------------- /examples/c05-error-handling.rs: -------------------------------------------------------------------------------- 1 | use rpc_router::{FromResources, IntoParams, RpcRequest, Router, RpcHandlerError, RpcParams, RpcResource, router_builder}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::json; 4 | use tokio::task::JoinSet; 5 | 6 | // -- Application Error 7 | 8 | // Must implement IntoHandlerError, here with RpcHandlerError derive macro 9 | #[derive(Debug, derive_more::Display, RpcHandlerError)] 10 | pub enum MyError { 11 | // TBC 12 | #[display("TitleCannotBeEmpty")] 13 | TitleCannotBeEmpty, 14 | } 15 | 16 | // -- Application Resources 17 | 18 | // Must implement FromResources, here with RpcResource derive macro 19 | #[derive(Clone, RpcResource)] 20 | pub struct ModelManager {} 21 | 22 | // Or can implement FromResources manual (default implementation provided) 23 | #[derive(Clone)] 24 | pub struct AiManager {} 25 | impl FromResources for AiManager {} 26 | 27 | // -- Params 28 | 29 | // Must implement IntoParams, here with RpcParams derive macro 30 | #[derive(Serialize, Deserialize, RpcParams)] 31 | pub struct TaskForCreate { 32 | title: String, 33 | done: Option, 34 | } 35 | 36 | // Or can implement FromResources manual (default implementation provided) 37 | // Another type that can be used as handler params argument. 38 | // Typicaly, all apis that just get data by `id` 39 | #[derive(Deserialize)] 40 | pub struct ParamsIded { 41 | pub id: i64, 42 | } 43 | impl IntoParams for ParamsIded {} 44 | 45 | // -- Application Return Values 46 | 47 | // Handler returned value must implement Serialize 48 | #[derive(Serialize)] 49 | pub struct Task { 50 | id: i64, 51 | title: String, 52 | done: bool, 53 | } 54 | 55 | // -- Application handler functions 56 | 57 | // An application rpc handler. Can take up to 8 arguments that have been marked RpcResource. 58 | // And have optionally have a last argument of a RpcParams 59 | pub async fn create_task(_mm: ModelManager, _aim: AiManager, params: TaskForCreate) -> Result { 60 | if params.title.is_empty() { 61 | return Err(MyError::TitleCannotBeEmpty); 62 | } 63 | Ok(123) // return fake id 64 | } 65 | 66 | // Another handler, does not have to have the same resource arguments. 67 | pub async fn get_task(_mm: ModelManager, params: ParamsIded) -> Result { 68 | Ok(Task { 69 | id: params.id, 70 | title: "fake task".to_string(), 71 | done: false, 72 | }) 73 | } 74 | 75 | // -- Main example implementation 76 | 77 | #[tokio::main] 78 | async fn main() -> Result<(), Box> { 79 | // -- Build router (can be built with Router::builder().add(..)) 80 | let rpc_router: Router = router_builder!(create_task, get_task) 81 | .append_resource(ModelManager {}) 82 | .append_resource(AiManager {}) 83 | .build(); 84 | 85 | // -- Simulate RPC request intakes (typically manage by the Web/IPC layer) 86 | let rpc_requests: Vec = vec![ 87 | // create_task request 88 | json!({ 89 | "jsonrpc": "2.0", 90 | "id": null, // json-rpc request id, generated by the client. Can be null, but has to be present. 91 | "method": "create_task", 92 | "params": { "title": "First task" } 93 | }) 94 | .try_into()?, 95 | // get_task request 96 | json!({ 97 | "jsonrpc": "2.0", 98 | "id": "some-string", // Can be string. Does not have to be unique from a server side. 99 | "method": "get_task", 100 | "params": { "id": 234 } 101 | }) 102 | .try_into()?, 103 | // create_task with invalid name request 104 | json!({ 105 | "jsonrpc": "2.0", 106 | "id": 123, // Can be number as well. 107 | "method": "create_task", 108 | "params": { "title": "" } 109 | }) 110 | .try_into()?, 111 | ]; 112 | 113 | // -- Simulate async processing (typically managed by the Web/IPC layer) 114 | let mut joinset = JoinSet::new(); 115 | for (idx, rpc_request) in rpc_requests.into_iter().enumerate() { 116 | let rpc_router = rpc_router.clone(); // Just Arc clone. 117 | joinset.spawn(async move { 118 | // Cheap way to "ensure" start spawns matches join_next order. (not for prod) 119 | tokio::time::sleep(std::time::Duration::from_millis(idx as u64 * 10)).await; 120 | 121 | // Do the router call 122 | rpc_router.call(rpc_request).await 123 | }); 124 | } 125 | 126 | // -- Print the result 127 | while let Some(Ok(call_result)) = joinset.join_next().await { 128 | match call_result { 129 | Ok(call_response) => println!("success: {call_response:?}"), 130 | Err(call_error) => { 131 | println!("Error for request id: {}, method: {}", call_error.id, call_error.method); 132 | match call_error.error { 133 | // It's a application handler type wrapped in a rpc_router CallError 134 | // we need to know it's type to extract it. 135 | rpc_router::Error::Handler(mut handler_error) => { 136 | // We can remove it not needed anymore 137 | if let Some(my_error) = handler_error.remove::() { 138 | println!("MyError: {my_error:?}") 139 | } else { 140 | println!("Unhandled App Error: {handler_error:?}"); 141 | } 142 | } 143 | // if it is other rpc_router error, can be handled normally 144 | other => println!("{other:?}"), 145 | } 146 | } 147 | } 148 | println!(); 149 | } 150 | 151 | Ok(()) 152 | } 153 | -------------------------------------------------------------------------------- /src/rpc_message/request.rs: -------------------------------------------------------------------------------- 1 | use crate::support::get_json_type; 2 | use crate::{RpcId, RpcRequestParsingError}; 3 | use serde::ser::SerializeStruct; 4 | use serde::{Deserialize, Serializer}; 5 | use serde_json::Value; 6 | 7 | /// The raw JSON-RPC request object, serving as the foundation for RPC routing. 8 | #[derive(Deserialize, Clone, Debug)] 9 | pub struct RpcRequest { 10 | pub id: RpcId, 11 | pub method: String, 12 | pub params: Option, 13 | } 14 | 15 | impl RpcRequest { 16 | pub fn new(id: impl Into, method: impl Into, params: Option) -> Self { 17 | RpcRequest { 18 | id: id.into(), 19 | method: method.into(), 20 | params, 21 | } 22 | } 23 | } 24 | 25 | /// Custom parser (probably need to be a deserializer) 26 | impl RpcRequest { 27 | pub fn from_value(value: Value) -> Result { 28 | RpcRequest::from_value_with_checks(value, RpcRequestCheckFlags::ALL) 29 | } 30 | 31 | /// Will perform the `jsonrpc: "2.0"` validation and parse the request. 32 | /// If this is not desired, using the standard `serde_json::from_value` would do the parsing 33 | /// and ignore `jsonrpc` property. 34 | pub fn from_value_with_checks( 35 | value: Value, 36 | checks: RpcRequestCheckFlags, 37 | ) -> Result { 38 | // TODO: When capturing the Value, we might implement a safeguard to prevent capturing Value Object or arrays 39 | // as they can be indefinitely large. One technical solution would be to replace the value with a String, 40 | // using something like `"[object/array redacted, 'id' should be of type number, string or null]"` as the string. 41 | let value_type = get_json_type(&value); 42 | 43 | let Value::Object(mut obj) = value else { 44 | return Err(RpcRequestParsingError::RequestInvalidType { 45 | actual_type: value_type.to_string(), 46 | }); 47 | }; 48 | 49 | // -- Check and remove `jsonrpc` property 50 | if checks.contains(RpcRequestCheckFlags::VERSION) { 51 | // -- Check and remove `jsonrpc` property 52 | match obj.remove("jsonrpc") { 53 | Some(version) => { 54 | if version.as_str().unwrap_or_default() != "2.0" { 55 | let (id_val, method) = extract_id_value_and_method(obj); 56 | return Err(RpcRequestParsingError::VersionInvalid { 57 | id: id_val, 58 | method, 59 | version, 60 | }); 61 | } 62 | } 63 | None => { 64 | let (id_val, method) = extract_id_value_and_method(obj); 65 | return Err(RpcRequestParsingError::VersionMissing { id: id_val, method }); 66 | } 67 | } 68 | } 69 | 70 | // -- Extract Raw Value Id for now 71 | let rpc_id_value: Option = obj.remove("id"); 72 | 73 | // -- Check method presence and type 74 | let method = match obj.remove("method") { 75 | None => { 76 | return Err(RpcRequestParsingError::MethodMissing { id: rpc_id_value }); 77 | } 78 | Some(method_val) => match method_val { 79 | Value::String(method_name) => method_name, 80 | other => { 81 | return Err(RpcRequestParsingError::MethodInvalidType { 82 | id: rpc_id_value, 83 | method: other, 84 | }); 85 | } 86 | }, 87 | }; 88 | 89 | // -- Process RpcId 90 | // Note: here if we do not have the check_id flag, we are permissive on the rpc_id, and 91 | let check_id = checks.contains(RpcRequestCheckFlags::ID); 92 | let id = match rpc_id_value { 93 | None => { 94 | if check_id { 95 | return Err(RpcRequestParsingError::IdMissing { method: Some(method) }); 96 | } else { 97 | RpcId::Null 98 | } 99 | } 100 | Some(id_value) => match RpcId::from_value(id_value) { 101 | Ok(rpc_id) => rpc_id, 102 | Err(err) => { 103 | if check_id { 104 | return Err(err); 105 | } else { 106 | RpcId::Null 107 | } 108 | } 109 | }, 110 | }; 111 | 112 | // -- Extract params (can be absent, which is valid) 113 | let params = obj.get_mut("params").map(Value::take); 114 | 115 | Ok(RpcRequest { id, method, params }) 116 | } 117 | } 118 | 119 | // region: --- Serialize Custom 120 | 121 | impl serde::Serialize for RpcRequest { 122 | fn serialize(&self, serializer: S) -> Result 123 | where 124 | S: Serializer, 125 | { 126 | // Determine the number of fields: jsonrpc, id, method are always present. params is optional. 127 | let mut field_count = 3; 128 | if self.params.is_some() { 129 | field_count += 1; 130 | } 131 | 132 | let mut state = serializer.serialize_struct("RpcRequest", field_count)?; 133 | 134 | // Always add "jsonrpc": "2.0" 135 | state.serialize_field("jsonrpc", "2.0")?; 136 | 137 | state.serialize_field("id", &self.id)?; 138 | state.serialize_field("method", &self.method)?; 139 | 140 | // Serialize params only if it's Some 141 | if let Some(params) = &self.params { 142 | state.serialize_field("params", params)?; 143 | } 144 | 145 | state.end() 146 | } 147 | } 148 | 149 | // endregion: --- Serialize Custom 150 | 151 | bitflags::bitflags! { 152 | /// Represents a set of flags. 153 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 154 | pub struct RpcRequestCheckFlags: u32 { 155 | /// Check the `jsonrpc = "2.0"` property 156 | const VERSION = 0b00000001; 157 | /// Check that `id` property is a valid rpc id (string, number, or null) 158 | /// NOTE: Does not support floating number (although the spec does) 159 | const ID = 0b00000010; 160 | 161 | /// Check all, te ID and VERSION 162 | const ALL = Self::VERSION.bits() | Self::ID.bits(); 163 | } 164 | } 165 | 166 | // region: --- Support 167 | 168 | // Extract (remove) the id and method. 169 | fn extract_id_value_and_method(mut obj: serde_json::Map) -> (Option, Option) { 170 | let id = obj.remove("id"); 171 | // for now be permisive with the method name, so as_str 172 | let method = obj.remove("method").and_then(|v| v.as_str().map(|s| s.to_string())); 173 | (id, method) 174 | } 175 | 176 | /// Convenient TryFrom, and will execute the Request::from_value, 177 | /// which will perform the version validation. 178 | impl TryFrom for RpcRequest { 179 | type Error = RpcRequestParsingError; 180 | fn try_from(value: Value) -> Result { 181 | RpcRequest::from_value(value) 182 | } 183 | } 184 | 185 | // endregion: --- Support 186 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # rpc-router 2 | 3 | **rpc-router** is a simple, light, type-safe, and macro-free JSON-RPC router library designed for Rust asynchronous applications. 4 | 5 | It provides a flexible way to define and route JSON-RPC requests to corresponding handler functions, managing resources, parameters, and error handling efficiently. 6 | 7 | **Key Features:** 8 | 9 | - **Type-Safe Handlers:** Define RPC handlers as regular async Rust functions with typed parameters derived from request resources and JSON-RPC parameters. 10 | - **Resource Management:** Inject shared resources (like database connections, configuration, etc.) into handlers using the `FromResources` trait. 11 | - **Parameter Handling:** Automatically deserialize JSON-RPC `params` into Rust types using the `IntoParams` trait. Supports optional parameters and default values via `IntoDefaultRpcParams`. 12 | - **Composable Routers:** Combine multiple routers using `RouterBuilder::extend` for modular application design. 13 | - **Error Handling:** Robust error handling with distinct types for routing errors (`rpc_router::Error`) and handler-specific errors (`HandlerError`), allowing applications to retain and inspect original error types. 14 | - **JSON-RPC Response Types:** Provides `RpcResponse`, `RpcError`, and `RpcResponseParsingError` for representing and parsing standard JSON-RPC 2.0 responses, with direct conversion from router `CallResult`. 15 | - **Ergonomic Macros (Optional):** Includes helper macros like `router_builder!` and `resources_builder!` for concise router and resource setup (can be used without macros as well). 16 | - **Minimal Dependencies:** Core library has minimal dependencies (primarily `serde`, `serde_json`, and `futures`). 17 | 18 | ## Core Concepts 19 | 20 | 1. **`Router`:** The central component that holds RPC method routes and associated handlers. It's built using `RouterBuilder`. 21 | 2. **`Handler` Trait:** Implemented automatically for async functions matching specific signatures. Handles resource extraction, parameter parsing, and execution logic. 22 | 3. **`Resources`:** A type map holding shared application state (e.g., database pools, config). Handlers access resources via types implementing `FromResources`. 23 | 4. **`FromResources` Trait:** Types implementing this trait can be injected as parameters into RPC handlers, fetched from the `Resources` map. Use `#[derive(RpcResource)]` for convenience. 24 | 5. **`IntoParams` Trait:** Types implementing this trait can be used as the final parameter in an RPC handler to receive and deserialize the JSON-RPC `params` field. Use `#[derive(RpcParams)]` for simple deserialization or implement the trait manually for custom logic. 25 | 6. **`CallResult`:** The `Result` type returned by `router.call(...)`, containing either `CallSuccess` (on success) or `CallError` (on failure). Includes the original `RpcId` and method name for context. 26 | 7. **`RpcResponse`:** Represents a standard JSON-RPC 2.0 response object (success or error). Can be easily created from a `CallResult`. 27 | 8. **`HandlerError`:** A wrapper for application-specific errors returned by handlers, allowing the application layer to recover the original error type. Use `#[derive(RpcHandlerError)]` on your custom error enums. 28 | 29 | ## Usage Example 30 | 31 | ```rust 32 | use rpc_router::{resources_builder, router_builder, FromResources, IntoParams, HandlerResult, RpcRequest, RpcResource, RpcParams, RpcResponse}; 33 | use serde::{Deserialize, Serialize}; 34 | use serde_json::json; 35 | 36 | // --- Define Resources 37 | #[derive(Clone, RpcResource)] 38 | struct AppState { 39 | version: String, 40 | } 41 | 42 | #[derive(Clone, RpcResource)] 43 | struct DbPool { 44 | // ... database connection pool 45 | url: String, // Example field 46 | } 47 | 48 | // --- Define Params 49 | #[derive(Deserialize, Serialize, RpcParams)] 50 | struct HelloParams { 51 | name: String, 52 | } 53 | 54 | // --- Define Custom Error (Optional) 55 | #[derive(Debug, thiserror::Error, rpc_router::RpcHandlerError)] 56 | enum MyHandlerError { 57 | #[error("Something went wrong: {0}")] 58 | SpecificError(String), 59 | } 60 | 61 | 62 | // --- Define RPC Handlers 63 | async fn hello(state: AppState, params: HelloParams) -> HandlerResult { 64 | // Use injected resources and parsed params 65 | Ok(format!("Hello {}, from app version {}!", params.name, state.version)) 66 | } 67 | 68 | async fn get_db_url(db: DbPool) -> HandlerResult { 69 | Ok(db.url.clone()) 70 | } 71 | 72 | // Example handler returning a custom error 73 | async fn might_fail() -> HandlerResult { 74 | // Simulate an error condition 75 | if rand::random() { // Requires `rand` crate 76 | Err(MyHandlerError::SpecificError("Random failure".to_string())) 77 | } else { 78 | Ok(42) 79 | } 80 | } 81 | 82 | 83 | #[tokio::main] 84 | async fn main() -> Result<(), Box> { 85 | // --- Create Resources 86 | let resources = resources_builder![ 87 | AppState { version: "1.0".to_string() }, 88 | DbPool { url: "dummy-db-url".to_string() } 89 | ].build(); 90 | 91 | // --- Create Router 92 | let router = router_builder![ 93 | handlers: [hello, get_db_url, might_fail] 94 | ].build(); 95 | 96 | // --- Simulate an RPC Call 97 | let request_json = json!({ 98 | "jsonrpc": "2.0", 99 | "id": 1, 100 | "method": "hello", 101 | "params": {"name": "World"} 102 | }); 103 | let rpc_request = RpcRequest::from_value(request_json)?; 104 | 105 | // Execute the call using the router's base resources 106 | let call_result = router.call_with_resources(rpc_request, resources).await; 107 | 108 | // --- Process the Result 109 | let rpc_response = RpcResponse::from(call_result); // Convert CallResult to RpcResponse 110 | 111 | // Serialize the response (e.g., to send back to the client) 112 | let response_json = serde_json::to_string_pretty(&rpc_response)?; 113 | println!("JSON-RPC Response:\n{}", response_json); 114 | 115 | // --- Example: Inspecting a specific error 116 | let fail_request_json = json!({ 117 | "jsonrpc": "2.0", 118 | "id": 2, 119 | "method": "might_fail", 120 | "params": null // Or omit params if handler doesn't take them 121 | }); 122 | let fail_rpc_request = RpcRequest::from_value(fail_request_json)?; 123 | let fail_call_result = router.call(fail_rpc_request).await; // Using router's own resources 124 | 125 | if let Err(call_error) = fail_call_result { 126 | // Access the router's error type 127 | if let rpc_router::Error::Handler(handler_error) = call_error.error { 128 | // Try to downcast to your specific handler error 129 | if let Some(my_error) = handler_error.get::() { 130 | println!("Caught specific handler error: {:?}", my_error); 131 | match my_error { 132 | MyHandlerError::SpecificError(msg) => { 133 | println!("Specific error message: {}", msg); 134 | } 135 | } 136 | } else { 137 | println!("Caught a handler error, but not MyHandlerError: {}", handler_error.type_name()); 138 | } 139 | } else { 140 | println!("Caught a router error: {:?}", call_error.error); 141 | } 142 | } 143 | 144 | 145 | Ok(()) 146 | } 147 | ``` 148 | 149 | *(Note: The example uses `tokio` for the async runtime and `thiserror` for simplified error definition, but `rpc-router` itself is runtime-agnostic and doesn't require `thiserror`)*. 150 | 151 | ## Learn More 152 | 153 | - [**API Documentation (`doc/all-apis.md`)**](./all-apis.md): Detailed explanation of all public types, traits, and functions. 154 | - [**Examples (`/examples`)**](../examples): Practical code examples demonstrating various features. 155 | 156 | ## Contributing 157 | 158 | Contributions are welcome! Please feel free to submit issues or pull requests. 159 | 160 | ## License 161 | 162 | Licensed under either of [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) or [MIT license](http://opensource.org/licenses/MIT) at your option. 163 | 164 | -------------------------------------------------------------------------------- /src/rpc_id.rs: -------------------------------------------------------------------------------- 1 | use crate::RpcRequestParsingError; 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 | use serde_json::Value; 4 | use std::sync::Arc; 5 | use uuid::Uuid; 6 | 7 | /// Represents a JSON-RPC 2.0 Request ID, which can be a String, Number, or Null. 8 | /// Uses `Arc` for strings to allow for efficient cloning, especially when the 9 | /// ID is part of request/response structures that might be cloned (e.g., in tracing/logging). 10 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 11 | pub enum RpcId { 12 | String(Arc), 13 | Number(i64), 14 | Null, 15 | } 16 | 17 | // region: --- ID Constructors 18 | 19 | impl RpcId { 20 | /// Generate a new ID given a scheme kind and encoding. 21 | pub fn from_scheme(kind: IdSchemeKind, enc: IdSchemeEncoding) -> Self { 22 | let s = enc.encode(kind.generate()); 23 | RpcId::String(Arc::from(s)) 24 | } 25 | 26 | // region: --- Uuid Convenient Constructors 27 | 28 | pub fn new_uuid_v4() -> Self { 29 | Self::from_scheme(IdSchemeKind::UuidV4, IdSchemeEncoding::Standard) 30 | } 31 | pub fn new_uuid_v4_base64() -> Self { 32 | Self::from_scheme(IdSchemeKind::UuidV4, IdSchemeEncoding::Base64) 33 | } 34 | pub fn new_uuid_v4_base64url() -> Self { 35 | Self::from_scheme(IdSchemeKind::UuidV4, IdSchemeEncoding::Base64UrlNoPad) 36 | } 37 | pub fn new_uuid_v4_base58() -> Self { 38 | Self::from_scheme(IdSchemeKind::UuidV4, IdSchemeEncoding::Base58) 39 | } 40 | 41 | pub fn new_uuid_v7() -> Self { 42 | Self::from_scheme(IdSchemeKind::UuidV7, IdSchemeEncoding::Standard) 43 | } 44 | pub fn new_uuid_v7_base64() -> Self { 45 | Self::from_scheme(IdSchemeKind::UuidV7, IdSchemeEncoding::Base64) 46 | } 47 | pub fn new_uuid_v7_base64url() -> Self { 48 | Self::from_scheme(IdSchemeKind::UuidV7, IdSchemeEncoding::Base64UrlNoPad) 49 | } 50 | pub fn new_uuid_v7_base58() -> Self { 51 | Self::from_scheme(IdSchemeKind::UuidV7, IdSchemeEncoding::Base58) 52 | } 53 | 54 | // endregion: --- Uuid Convenient Constructors 55 | } 56 | 57 | /// Pick the ID scheme you want. 58 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 59 | pub enum IdSchemeKind { 60 | UuidV4, 61 | UuidV7, 62 | // Ulid, Snowflake, Nanoid, etc. can be added later 63 | } 64 | 65 | /// Pick your base-encoding: 66 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 67 | pub enum IdSchemeEncoding { 68 | Standard, 69 | Base64, 70 | Base64UrlNoPad, 71 | Base58, 72 | } 73 | 74 | impl IdSchemeKind { 75 | fn generate(&self) -> Vec { 76 | match self { 77 | IdSchemeKind::UuidV4 => Uuid::new_v4().as_bytes().to_vec(), 78 | IdSchemeKind::UuidV7 => Uuid::now_v7().as_bytes().to_vec(), 79 | // Add other generators later... 80 | } 81 | } 82 | } 83 | 84 | impl IdSchemeEncoding { 85 | /// Turn a byte slice into your desired string form. 86 | fn encode(&self, bytes: Vec) -> String { 87 | match self { 88 | IdSchemeEncoding::Standard => { 89 | // Assume bytes come from UUID; reparse to UUID for pretty string. 90 | Uuid::from_slice(&bytes).map(|u| u.to_string()).unwrap_or_default() 91 | } 92 | IdSchemeEncoding::Base64 => data_encoding::BASE64.encode(&bytes), 93 | IdSchemeEncoding::Base64UrlNoPad => data_encoding::BASE64URL_NOPAD.encode(&bytes), 94 | IdSchemeEncoding::Base58 => bs58::encode(&bytes).into_string(), 95 | } 96 | } 97 | } 98 | 99 | // endregion: --- ID Constructors 100 | 101 | // region: --- std Display 102 | 103 | impl core::fmt::Display for RpcId { 104 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 105 | match self { 106 | RpcId::String(s) => write!(f, "{}", s), 107 | RpcId::Number(n) => write!(f, "{}", n), 108 | RpcId::Null => write!(f, "null"), 109 | } 110 | } 111 | } 112 | 113 | // endregion: --- std Display 114 | 115 | // -- Conversions 116 | 117 | impl RpcId { 118 | /// Converts the `RpcId` into a `serde_json::Value`. Infallible. 119 | pub fn to_value(&self) -> Value { 120 | match self { 121 | RpcId::String(s) => Value::String(s.to_string()), 122 | RpcId::Number(n) => Value::Number((*n).into()), 123 | RpcId::Null => Value::Null, 124 | } 125 | } 126 | 127 | /// Attempts to convert a `serde_json::Value` into an `RpcId`. 128 | /// Returns `Error::RpcIdInvalid` if the `value` is not a String, Number, or Null. 129 | pub fn from_value(value: Value) -> core::result::Result { 130 | match value { 131 | Value::String(s) => Ok(RpcId::String(s.into())), 132 | Value::Number(n) => n.as_i64().map(RpcId::Number).ok_or_else(|| RpcRequestParsingError::IdInvalid { 133 | actual: format!("{n}"), 134 | cause: "Number is not a valid i64".into(), 135 | }), 136 | Value::Null => Ok(RpcId::Null), 137 | _ => Err(RpcRequestParsingError::IdInvalid { 138 | actual: format!("{value:?}"), 139 | cause: "ID must be a String, Number, or Null".into(), 140 | }), 141 | } 142 | } 143 | } 144 | 145 | // -- Default 146 | 147 | impl Default for RpcId { 148 | /// Defaults to `RpcId::Null`. 149 | fn default() -> Self { 150 | RpcId::Null 151 | } 152 | } 153 | 154 | // -- Serde 155 | 156 | impl Serialize for RpcId { 157 | fn serialize(&self, serializer: S) -> core::result::Result 158 | where 159 | S: Serializer, 160 | { 161 | match self { 162 | RpcId::String(s) => serializer.serialize_str(s), 163 | RpcId::Number(n) => serializer.serialize_i64(*n), 164 | RpcId::Null => serializer.serialize_none(), 165 | } 166 | } 167 | } 168 | 169 | impl<'de> Deserialize<'de> for RpcId { 170 | fn deserialize(deserializer: D) -> core::result::Result 171 | where 172 | D: Deserializer<'de>, 173 | { 174 | let value = Value::deserialize(deserializer)?; 175 | RpcId::from_value(value).map_err(serde::de::Error::custom) 176 | } 177 | } 178 | 179 | // -- From Implementations 180 | 181 | impl From for RpcId { 182 | fn from(s: String) -> Self { 183 | RpcId::String(s.into()) 184 | } 185 | } 186 | 187 | impl From<&str> for RpcId { 188 | fn from(s: &str) -> Self { 189 | RpcId::String(s.into()) 190 | } 191 | } 192 | 193 | impl From for RpcId { 194 | fn from(n: i64) -> Self { 195 | RpcId::Number(n) 196 | } 197 | } 198 | 199 | impl From for RpcId { 200 | fn from(n: i32) -> Self { 201 | RpcId::Number(n as i64) 202 | } 203 | } 204 | 205 | impl From for RpcId { 206 | fn from(n: u32) -> Self { 207 | RpcId::Number(n as i64) 208 | } 209 | } 210 | 211 | // region: --- Tests 212 | 213 | #[cfg(test)] 214 | mod tests { 215 | use super::*; 216 | use serde_json::{from_value, json, to_value}; 217 | 218 | type TestResult = core::result::Result>; // For tests. 219 | 220 | #[test] 221 | fn test_rpc_id_ser_de() -> TestResult<()> { 222 | // -- Setup & Fixtures 223 | let ids = [ 224 | RpcId::String("id-1".into()), 225 | RpcId::Number(123), 226 | RpcId::Null, 227 | RpcId::String("".into()), // Empty string 228 | ]; 229 | let expected_values = [ 230 | json!("id-1"), 231 | json!(123), 232 | json!(null), 233 | json!(""), // Empty string JSON 234 | ]; 235 | 236 | // -- Exec & Check 237 | for (i, id) in ids.iter().enumerate() { 238 | let value = to_value(id)?; 239 | assert_eq!(value, expected_values[i], "Serialization check for id[{i}]"); 240 | 241 | let deserialized_id: RpcId = from_value(value.clone())?; 242 | assert_eq!(&deserialized_id, id, "Deserialization check for id[{i}]"); 243 | 244 | let from_value_id = RpcId::from_value(value)?; 245 | assert_eq!(from_value_id, *id, "from_value check for id[{i}]"); 246 | } 247 | 248 | Ok(()) 249 | } 250 | 251 | #[test] 252 | fn test_rpc_id_from_value_invalid() -> TestResult<()> { 253 | // -- Setup & Fixtures 254 | let invalid_values = vec![ 255 | json!(true), 256 | json!(123.45), // Float number 257 | json!([1, 2]), 258 | json!({"a": 1}), 259 | ]; 260 | 261 | // -- Exec & Check 262 | for value in invalid_values { 263 | let res = RpcId::from_value(value.clone()); 264 | assert!( 265 | matches!(res, Err(RpcRequestParsingError::IdInvalid { .. })), 266 | "Expected RpcIdInvalid for value: {:?}", 267 | value 268 | ); 269 | } 270 | 271 | Ok(()) 272 | } 273 | 274 | #[test] 275 | fn test_rpc_id_to_value() -> TestResult<()> { 276 | // -- Setup & Fixtures 277 | let id_str = RpcId::String("hello".into()); 278 | let id_num = RpcId::Number(42); 279 | let id_null = RpcId::Null; 280 | 281 | // -- Exec 282 | let val_str = id_str.to_value(); 283 | let val_num = id_num.to_value(); 284 | let val_null = id_null.to_value(); 285 | 286 | // -- Check 287 | assert_eq!(val_str, json!("hello")); 288 | assert_eq!(val_num, json!(42)); 289 | assert_eq!(val_null, json!(null)); 290 | 291 | Ok(()) 292 | } 293 | 294 | #[test] 295 | fn test_rpc_id_from_impls() -> TestResult<()> { 296 | // -- Check String/&str 297 | assert_eq!(RpcId::from("test_str"), RpcId::String("test_str".into())); 298 | assert_eq!( 299 | RpcId::from(String::from("test_string")), 300 | RpcId::String("test_string".into()) 301 | ); 302 | 303 | // -- Check numbers 304 | assert_eq!(RpcId::from(100i64), RpcId::Number(100)); 305 | assert_eq!(RpcId::from(200i32), RpcId::Number(200)); 306 | assert_eq!(RpcId::from(300u32), RpcId::Number(300)); 307 | 308 | Ok(()) 309 | } 310 | 311 | #[test] 312 | fn test_rpc_id_default() -> TestResult<()> { 313 | // -- Check 314 | assert_eq!(RpcId::default(), RpcId::Null); 315 | 316 | Ok(()) 317 | } 318 | } 319 | 320 | // endregion: --- Tests 321 | -------------------------------------------------------------------------------- /src/resource/resources_inner.rs: -------------------------------------------------------------------------------- 1 | //! rpc-router Notes: 2 | //! - Currently, this is mostly copy of the `extensions.rs` from the `http@1.1.0` crate. 3 | //! - Only visible to `crate::resource` and encapsulated within `RpcResponses`. 4 | //! - Subject to future changes. Ideally, it should be a type map. 5 | //! - Presently, added a `_..` prefix to all unused functions. 6 | //! - Decisions on what to retain or discard will be made later. 7 | 8 | use std::any::{Any, TypeId}; 9 | use std::collections::HashMap; 10 | use std::fmt; 11 | use std::hash::{BuildHasherDefault, Hasher}; 12 | 13 | type AnyMap = HashMap, BuildHasherDefault>; 14 | 15 | // With TypeIds as keys, there's no need to hash them. They are already hashes 16 | // themselves, coming from the compiler. The IdHasher just holds the u64 of 17 | // the TypeId, and then returns it, instead of doing any bit fiddling. 18 | #[derive(Default)] 19 | struct IdHasher(u64); 20 | 21 | impl Hasher for IdHasher { 22 | fn write(&mut self, _: &[u8]) { 23 | unreachable!("TypeId calls write_u64"); 24 | } 25 | 26 | #[inline] 27 | fn write_u64(&mut self, id: u64) { 28 | self.0 = id; 29 | } 30 | 31 | #[inline] 32 | fn finish(&self) -> u64 { 33 | self.0 34 | } 35 | } 36 | 37 | /// A type map of protocol extensions. 38 | /// 39 | /// `Extensions` can be used by `Request` and `Response` to store 40 | /// extra data derived from the underlying protocol. 41 | #[derive(Clone, Default)] 42 | pub(crate) struct ResourcesInner { 43 | // If extensions are never used, no need to carry around an empty HashMap. 44 | // That's 3 words. Instead, this is only 1 word. 45 | map: Option>, 46 | } 47 | 48 | // Note: Cherry picked functions from `extensions.rs` that is used in this code base. 49 | // The rest are prefixed with `_` for now. 50 | impl ResourcesInner { 51 | /// Insert a type into this `Extensions`. 52 | /// 53 | /// If a extension of this type already existed, it will 54 | /// be returned. 55 | /// 56 | /// # Example 57 | /// 58 | /// ``` 59 | /// # use http::Extensions; 60 | /// let mut ext = Extensions::new(); 61 | /// assert!(ext.insert(5i32).is_none()); 62 | /// assert!(ext.insert(4u8).is_none()); 63 | /// assert_eq!(ext.insert(9i32), Some(5i32)); 64 | /// ``` 65 | pub fn insert(&mut self, val: T) -> Option { 66 | self.map 67 | .get_or_insert_with(Box::default) 68 | .insert(TypeId::of::(), Box::new(val)) 69 | .and_then(|boxed| boxed.into_any().downcast().ok().map(|boxed| *boxed)) 70 | } 71 | 72 | /// Get a reference to a type previously inserted on this `Extensions`. 73 | /// 74 | /// # Example 75 | /// 76 | /// ``` 77 | /// # use http::Extensions; 78 | /// let mut ext = Extensions::new(); 79 | /// assert!(ext.get::().is_none()); 80 | /// ext.insert(5i32); 81 | /// 82 | /// assert_eq!(ext.get::(), Some(&5i32)); 83 | /// ``` 84 | pub fn get(&self) -> Option<&T> { 85 | self.map 86 | .as_ref() 87 | .and_then(|map| map.get(&TypeId::of::())) 88 | .and_then(|boxed| (**boxed).as_any().downcast_ref()) 89 | } 90 | 91 | /// Extends `self` with another `Extensions`. 92 | /// 93 | /// If an instance of a specific type exists in both, the one in `self` is overwritten with the 94 | /// one from `other`. 95 | /// 96 | /// # Example 97 | /// 98 | /// ``` 99 | /// # use http::Extensions; 100 | /// let mut ext_a = Extensions::new(); 101 | /// ext_a.insert(8u8); 102 | /// ext_a.insert(16u16); 103 | /// 104 | /// let mut ext_b = Extensions::new(); 105 | /// ext_b.insert(4u8); 106 | /// ext_b.insert("hello"); 107 | /// 108 | /// ext_a.extend(ext_b); 109 | /// assert_eq!(ext_a.len(), 3); 110 | /// assert_eq!(ext_a.get::(), Some(&4u8)); 111 | /// assert_eq!(ext_a.get::(), Some(&16u16)); 112 | /// assert_eq!(ext_a.get::<&'static str>().copied(), Some("hello")); 113 | /// ``` 114 | pub fn extend(&mut self, other: Self) { 115 | if let Some(other) = other.map { 116 | if let Some(map) = &mut self.map { 117 | map.extend(*other); 118 | } else { 119 | self.map = Some(other); 120 | } 121 | } 122 | } 123 | 124 | /// Check whether the extension set is empty or not. 125 | /// 126 | /// # Example 127 | /// 128 | /// ``` 129 | /// # use http::Extensions; 130 | /// let mut ext = Extensions::new(); 131 | /// assert!(ext.is_empty()); 132 | /// ext.insert(5i32); 133 | /// assert!(!ext.is_empty()); 134 | /// ``` 135 | #[inline] 136 | pub fn is_empty(&self) -> bool { 137 | self.map.as_ref().is_none_or(|map| map.is_empty()) 138 | } 139 | } 140 | 141 | // Note: Usued code for now (from `extensions.rs`, keep until refactor) 142 | #[allow(unused)] // For early development. 143 | impl ResourcesInner { 144 | /// Create an empty `Extensions`. 145 | #[inline] 146 | fn _new() -> ResourcesInner { 147 | ResourcesInner { map: None } 148 | } 149 | 150 | /// Get a mutable reference to a type previously inserted on this `Extensions`. 151 | /// 152 | /// # Example 153 | /// 154 | /// ``` 155 | /// # use http::Extensions; 156 | /// let mut ext = Extensions::new(); 157 | /// ext.insert(String::from("Hello")); 158 | /// ext.get_mut::().unwrap().push_str(" World"); 159 | /// 160 | /// assert_eq!(ext.get::().unwrap(), "Hello World"); 161 | /// ``` 162 | fn get_mut(&mut self) -> Option<&mut T> { 163 | self.map 164 | .as_mut() 165 | .and_then(|map| map.get_mut(&TypeId::of::())) 166 | .and_then(|boxed| (**boxed).as_any_mut().downcast_mut()) 167 | } 168 | 169 | /// Get a mutable reference to a type, inserting `value` if not already present on this 170 | /// `Extensions`. 171 | /// 172 | /// # Example 173 | /// 174 | /// ``` 175 | /// # use http::Extensions; 176 | /// let mut ext = Extensions::new(); 177 | /// *ext.get_or_insert(1i32) += 2; 178 | /// 179 | /// assert_eq!(*ext.get::().unwrap(), 3); 180 | /// ``` 181 | fn get_or_insert(&mut self, value: T) -> &mut T { 182 | self.get_or_insert_with(|| value) 183 | } 184 | 185 | /// Get a mutable reference to a type, inserting the value created by `f` if not already present 186 | /// on this `Extensions`. 187 | /// 188 | /// # Example 189 | /// 190 | /// ``` 191 | /// # use http::Extensions; 192 | /// let mut ext = Extensions::new(); 193 | /// *ext.get_or_insert_with(|| 1i32) += 2; 194 | /// 195 | /// assert_eq!(*ext.get::().unwrap(), 3); 196 | /// ``` 197 | fn get_or_insert_with T>(&mut self, f: F) -> &mut T { 198 | let out = self 199 | .map 200 | .get_or_insert_with(Box::default) 201 | .entry(TypeId::of::()) 202 | .or_insert_with(|| Box::new(f())); 203 | (**out).as_any_mut().downcast_mut().unwrap() 204 | } 205 | 206 | /// Get a mutable reference to a type, inserting the type's default value if not already present 207 | /// on this `Extensions`. 208 | /// 209 | /// # Example 210 | /// 211 | /// ``` 212 | /// # use http::Extensions; 213 | /// let mut ext = Extensions::new(); 214 | /// *ext.get_or_insert_default::() += 2; 215 | /// 216 | /// assert_eq!(*ext.get::().unwrap(), 2); 217 | /// ``` 218 | fn get_or_insert_default(&mut self) -> &mut T { 219 | self.get_or_insert_with(T::default) 220 | } 221 | 222 | /// Remove a type from this `Extensions`. 223 | /// 224 | /// If a extension of this type existed, it will be returned. 225 | /// 226 | /// # Example 227 | /// 228 | /// ``` 229 | /// # use http::Extensions; 230 | /// let mut ext = Extensions::new(); 231 | /// ext.insert(5i32); 232 | /// assert_eq!(ext.remove::(), Some(5i32)); 233 | /// assert!(ext.get::().is_none()); 234 | /// ``` 235 | fn remove(&mut self) -> Option { 236 | self.map 237 | .as_mut() 238 | .and_then(|map| map.remove(&TypeId::of::())) 239 | .and_then(|boxed| boxed.into_any().downcast().ok().map(|boxed| *boxed)) 240 | } 241 | 242 | /// Clear the `Extensions` of all inserted extensions. 243 | /// 244 | /// # Example 245 | /// 246 | /// ``` 247 | /// # use http::Extensions; 248 | /// let mut ext = Extensions::new(); 249 | /// ext.insert(5i32); 250 | /// ext.clear(); 251 | /// 252 | /// assert!(ext.get::().is_none()); 253 | /// ``` 254 | #[inline] 255 | fn clear(&mut self) { 256 | if let Some(ref mut map) = self.map { 257 | map.clear(); 258 | } 259 | } 260 | 261 | /// Get the numer of extensions available. 262 | /// 263 | /// # Example 264 | /// 265 | /// ``` 266 | /// # use http::Extensions; 267 | /// let mut ext = Extensions::new(); 268 | /// assert_eq!(ext.len(), 0); 269 | /// ext.insert(5i32); 270 | /// assert_eq!(ext.len(), 1); 271 | /// ``` 272 | #[inline] 273 | pub fn len(&self) -> usize { 274 | self.map.as_ref().map_or(0, |map| map.len()) 275 | } 276 | } 277 | 278 | impl fmt::Debug for ResourcesInner { 279 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 280 | f.debug_struct("Extensions").finish() 281 | } 282 | } 283 | 284 | trait AnyClone: Any { 285 | fn clone_box(&self) -> Box; 286 | fn as_any(&self) -> &dyn Any; 287 | fn as_any_mut(&mut self) -> &mut dyn Any; 288 | fn into_any(self: Box) -> Box; 289 | } 290 | 291 | impl AnyClone for T { 292 | fn clone_box(&self) -> Box { 293 | Box::new(self.clone()) 294 | } 295 | 296 | fn as_any(&self) -> &dyn Any { 297 | self 298 | } 299 | 300 | fn as_any_mut(&mut self) -> &mut dyn Any { 301 | self 302 | } 303 | 304 | fn into_any(self: Box) -> Box { 305 | self 306 | } 307 | } 308 | 309 | impl Clone for Box { 310 | fn clone(&self) -> Self { 311 | (**self).clone_box() 312 | } 313 | } 314 | 315 | #[test] 316 | fn test_extensions() { 317 | #[derive(Clone, Debug, PartialEq)] 318 | struct MyType(i32); 319 | 320 | let mut extensions = ResourcesInner::_new(); 321 | 322 | extensions.insert(5i32); 323 | extensions.insert(MyType(10)); 324 | 325 | assert_eq!(extensions.get(), Some(&5i32)); 326 | assert_eq!(extensions.get_mut(), Some(&mut 5i32)); 327 | 328 | let ext2 = extensions.clone(); 329 | 330 | assert_eq!(extensions.remove::(), Some(5i32)); 331 | assert!(extensions.get::().is_none()); 332 | 333 | // clone still has it 334 | assert_eq!(ext2.get(), Some(&5i32)); 335 | assert_eq!(ext2.get(), Some(&MyType(10))); 336 | 337 | assert_eq!(extensions.get::(), None); 338 | assert_eq!(extensions.get(), Some(&MyType(10))); 339 | } 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rpc-router - JSON-RPC Routing Library 2 | 3 | **WARNING**: The main branch is now a work in progress for the upcoming `v0.2.0` (see the [v0.1.1](https://github.com/jeremychone/rust-rpc-router/tree/v0.1.1) tag for the 0.1.x version). 4 | 5 | **v0.2.0-alpha.x** Will be released but will have API changes between them. The future `-rc.x` will be more stable. 6 | 7 | Upcoming API changes for `v0.2.0` 8 | - `RpcId` - Now uses a concrete type for RpcId. 9 | - `RpcRequest` - The old `Request` is now renamed `RpcRequest`. The design is that raw JSON-RPC constructs are prefixed with `Rpc`. 10 | - `RpcNotification` - New type (Like `RpcRequest` but with no `.id` as per the spec). 11 | - `RpcResponse` - New type. 12 | 13 | ## Getting Started 14 | 15 | `rpc-router` is a [JSON-RPC](https://www.jsonrpc.org/specification) routing library in Rust for asynchronous dynamic dispatch with support for variadic arguments (up to 8 resources + 1 optional parameter). (Code snippets below are from: [examples/c00-readme.rs](examples/c00-readme.rs)) 16 | 17 | The goal of this library is to enable application functions with different argument types and signatures as follows: 18 | 19 | ```rust 20 | pub async fn create_task(mm: ModelManager, aim: AiManager, params: TaskForCreate) -> Result { 21 | // ... 22 | } 23 | pub async fn get_task(mm: ModelManager, params: ParamsIded) -> Result { 24 | // ... 25 | } 26 | ``` 27 | 28 | To be callable from a JSON-RPC request as follows: 29 | 30 | ```rust 31 | // JSON-RPC request coming from Axum route payload, Tauri command params, ... 32 | let rpc_request = json!( 33 | { jsonrpc: "2.0", id: 1, // required by JSON-RPC 34 | method: "create_task", // method name (matches function name) 35 | params: {title: "First Task"} // optional params (last function argument) 36 | }).try_into()?; 37 | 38 | // Async execute the RPC request 39 | let call_response = rpc_router.call(rpc_request).await?; 40 | ``` 41 | 42 | For this, we just need to build the router, the resources, parse the JSON-RPC request, and execute the call from the router as follows: 43 | 44 | ```rust 45 | // Build the Router with the handlers and common resources 46 | let rpc_router = router_builder!( 47 | handlers: [get_task, create_task], // will be turned into routes 48 | resources: [ModelManager {}, AiManager {}] // common resources for all calls 49 | ) 50 | .build(); 51 | // Can do the same with `Router::builder().append(...)/append_resource(...)` 52 | 53 | // Create and parse rpc request example. 54 | let rpc_request: rpc_router::Request = json!({ 55 | "jsonrpc": "2.0", 56 | "id": "some-client-req-id", // JSON-RPC request id. Can be null, num, string, but must be present. 57 | "method": "create_task", 58 | "params": { "title": "First task" } // optional. 59 | }).try_into()?; 60 | 61 | // Async execute the RPC request. 62 | let call_response = rpc_router.call(rpc_resources, rpc_request).await?; 63 | 64 | // Or `call_with_resources` for additional per-call resources that override router common resources. 65 | // e.g., rpc_router.call_with_resources(rpc_request, additional_resources) 66 | 67 | // Display the response. 68 | let CallSuccess { id, method, value } = call_response; 69 | println!( 70 | r#"RPC call response: 71 | 72 | id: {id:?}, 73 | method: {method}, 74 | value: {value:?}, 75 | "# 76 | ); 77 | ``` 78 | 79 | See [examples/c00-readme.rs](examples/c00-readme.rs) for the complete working code. 80 | 81 | For the above to work, here are the requirements for the various types: 82 | 83 | - `ModelManager` and `AiManager` are rpc-router **Resources**. These types just need to implement `rpc_router::FromResources` (the trait has a default implementation, and `RpcResource` derive macros can generate this one-liner implementation). 84 | 85 | ```rust 86 | // Make it a Resource with RpcResource derive macro 87 | #[derive(Clone, RpcResource)] 88 | pub struct ModelManager {} 89 | 90 | // Make it a Resource by implementing FromResources 91 | #[derive(Clone)] 92 | pub struct AiManager {} 93 | impl FromResources for AiManager {} 94 | ``` 95 | 96 | - `TaskForCreate` and `ParamsIded` are used as JSON-RPC Params and must implement the `rpc_router::IntoParams` trait, which has a default implementation, and can also be implemented by `RpcParams` derive macros. 97 | 98 | ```rust 99 | // Make it a Params with RpcParams derive macro 100 | #[derive(Serialize, Deserialize, RpcParams)] 101 | pub struct TaskForCreate { 102 | title: String, 103 | done: Option, 104 | } 105 | 106 | // Make it a Params by implementing IntoParams 107 | #[derive(Deserialize)] 108 | pub struct ParamsIded { 109 | pub id: i64, 110 | } 111 | impl IntoParams for ParamsIded {} 112 | ``` 113 | 114 | - `Task`, as a returned value, just needs to implement `serde::Serialize` 115 | 116 | ```rust 117 | #[derive(Serialize)] 118 | pub struct Task { 119 | id: i64, 120 | title: String, 121 | done: bool, 122 | } 123 | ``` 124 | 125 | - `MyError` must implement `IntoHandlerError`, which also has a default implementation, and can also be implemented by `RpcHandlerError` derive macros. 126 | 127 | ```rust 128 | #[derive(Debug, thiserror::Error, RpcHandlerError)] 129 | pub enum MyError { 130 | // TBC 131 | #[error("TitleCannotBeEmpty")] 132 | TitleCannotBeEmpty, 133 | } 134 | ``` 135 | 136 | By the Rust type model, these application errors are set in the `HandlerError` and need to be retrieved by `handler_error.get::()`. See [examples/c05-error-handling.rs](examples/c05-error-handling.rs). 137 | 138 | Full code: [examples/c00-readme.rs](examples/c00-readme.rs) 139 | 140 | > **IMPORTANT** 141 | > 142 | > For the `0.1.x` releases, there may be some changes to types or API naming. Therefore, the version should be locked to the latest version used, for example, `=0.1.0`. I will try to keep changes to a minimum, if any, and document them in the future [CHANGELOG](CHANGELOG.md). 143 | > 144 | > Once `0.2.0` is released, I will adhere more strictly to semantic versioning. 145 | 146 | ### Concepts 147 | 148 | This library has the following main constructs: 149 | 150 | 1) `Router` - Router is the construct that holds all of the Handler Functions and can be invoked with `router.call(resources, rpc_request)`. Here are the two main ways to build a `Router` object: 151 | - **RouterBuilder** - via `RouterBuilder::default()` or `Router::builder()`, then call `.append(name, function)` or `.append_dyn(name, function.into_dyn())` to avoid type monomorphization at the "append" stage. 152 | - **router_builder!** - via the macro `router_builder!(function1, function2, ...)`. This will create, initialize, and return a `RouterBuilder` object. 153 | - In both cases, call `.build()` to construct the immutable, shareable (via inner Arc) `Router` object. 154 | 155 | 2) `Resources` - Resources is the type map construct that holds the resources that an RPC handler function might request. 156 | - It's similar to Axum State/RequestExtractor or the Tauri State model. In the case of `rpc-router`, there is one "domain space" for those states called resources. 157 | - It's built via `ResourcesBuilder::default().append(my_object)...build()`. 158 | - Or via the macro `resources_builder![my_object1, my_object2].build()`. 159 | - The `Resources` hold the type map in an `Arc<>` and are completely immutable and can be cloned effectively. 160 | - `ResourcesBuilder` is not wrapped in an `Arc<>`, and cloning it will clone the full type map. This can be very useful for sharing a common base resources builder across various calls while allowing each call to add more per-request resources. 161 | - All the values/objects inserted into the Resources must implement `Clone + Send + Sync + 'static` (here `'static` means the type cannot have any references other than static ones). 162 | 163 | 3) `Request` - Is the object that has the JSON-RPC Request `id`, `method`, and `params`. 164 | - To make a struct a `params`, it has to implement the `rpc_router::IntoParams` trait, which has a default implementation. 165 | - So, implement `impl rpc_router::IntoParams for ... {}` or `#[derive(RpcParams)]`. 166 | - `rpc_router::Request::from_value(serde_json::Value) -> Result` will return a `RequestParsingError` if the Value does not have `id: Value`, `method: String` or if the Value does not contain `"jsonrpc": "2.0"` as per the JSON-RPC spec. 167 | - `let request: rpc_router::Request = value.try_into()?` uses the same `from_value` validation steps. 168 | - Doing `serde_json::from_value::(value)` will not change the `jsonrpc`. 169 | 170 | 4) `Handler` - RPC handler functions can be any async application function that takes up to 8 resource arguments, plus an optional Params argument. 171 | - For example, `async fn create_task(_mm: ModelManager, aim: AiManager, params: TaskForCreate) -> MyResult` 172 | 173 | 5) `HandlerError` - RPC handler functions can return their own `Result` as long as the error type implements `IntoHandlerError`, which can be easily implemented as `rpc_router::HandlerResult` which includes an `impl IntoHandlerError for MyError {}`, or with the `RpcHandlerError` derive macro. 174 | - To allow handler functions to return their application error, `HandlerError` is essentially a type holder that allows the extraction of the application error with `handler_error.get()`. 175 | - This requires the application code to know which error type to extract but provides flexibility to return any Error type. 176 | - Typically, an application will have a few application error types for its handlers, so this ergonomic trade-off still has net positive value as it enables the use of application-specific error types. 177 | 178 | 6) `CallResult` - `router.call(...)` will return a `CallResult`, which is a `Result` where both include the JSON-RPC `id` and `method` name context for future processing. 179 | - `CallError` contains `.error: rpc_router::Error`, which includes `rpc_router::Error::Handler(HandlerError)` in the event of a handler error. 180 | - `CallSuccess` contains `.value: serde_json::Value`, which is the serialized value returned by a successful handler call. 181 | 182 | ### Derive Macros 183 | 184 | `rpc-router` has some convenient derive proc macros that generate the implementation of various traits. 185 | 186 | This is just a stylistic convenience, as the traits themselves have default implementations and are typically one-liner implementations. 187 | 188 | > Note: These derive proc macros are prefixed with `Rpc` since macros often have generic names, so the prefix adds clarity. Other `rpc-router` types are without the prefix to follow Rust customs. 189 | 190 | ### `#[derive(rpc_router::RpcParams)]` 191 | 192 | Implements `rpc_router::IntoParams` for the type. 193 | 194 | Works on simple types. 195 | 196 | ```rust 197 | #[derive(serde::Deserialize, rpc_router::RpcParams)] 198 | pub struct ParamsIded { 199 | id: i64 200 | } 201 | 202 | // Will generate: 203 | // impl rpc_router::IntoParams for ParamsIded {} 204 | ``` 205 | 206 | Works with generic types (all will be bound to `DeserializeOwned + Send`): 207 | 208 | ```rust 209 | #[derive(rpc_router::RpcParams)] 210 | pub struct ParamsForUpdate { 211 | id: i64, 212 | D 213 | } 214 | // Will generate 215 | // impl IntoParams for ParamsForUpdate where D: DeserializeOwned + Send {} 216 | ``` 217 | 218 | ### `#[derive(rpc_router::RpcResource)]` 219 | 220 | Implements the `rpc_router::FromResource` trait. 221 | 222 | ```rust 223 | #[derive(Clone, rpc_router::RpcResource)] 224 | pub struct ModelManager; 225 | 226 | // Will generate: 227 | // impl FromResources for ModelManager {} 228 | ``` 229 | 230 | The `FromResources` trait has a default implementation to get the `T` type (here `ModelManager`) from the `rpc_router::Resources` type map. 231 | 232 | ### `#[derive(rpc_router::RpcHandlerError)]` 233 | 234 | Implements the `rpc_router::IntoHandlerError` trait. 235 | 236 | ```rust 237 | #[derive(Debug, Serialize, RpcHandlerError)] 238 | pub enum MyError { 239 | InvalidName, 240 | // ... 241 | } 242 | 243 | // Will generate: 244 | // impl IntoHandlerError for MyError {} 245 | ``` 246 | 247 |
248 | 249 | ## Related Links 250 | 251 | - [GitHub Repo](https://github.com/jeremychone/rust-rpc-router) 252 | - [crates.io](https://crates.io/crates/rpc-router) 253 | - [Rust10x rust-web-app](https://rust10x.com/web-app) (web-app code blueprint using [rpc-router](https://github.com/jeremychone/rust-rpc-router) with [Axum](https://github.com/tokio-rs/axum)) -------------------------------------------------------------------------------- /src/rpc_message/notification.rs: -------------------------------------------------------------------------------- 1 | //! Represents a JSON-RPC Notification object. 2 | 3 | use crate::RpcRequest; 4 | use crate::rpc_message::rpc_request_parsing_error::RpcRequestParsingError; 5 | use crate::rpc_message::support::{extract_value, parse_method, parse_params, validate_version}; 6 | use crate::support::get_json_type; 7 | use serde::ser::SerializeStruct; 8 | use serde::{Deserialize, Serialize, Serializer}; 9 | use serde_json::Value; 10 | 11 | /// Represents a JSON-RPC Notification object, which is a request without an `id`. 12 | /// Notifications are inherently unreliable as no response is expected. 13 | /// 14 | /// Note: Derives `Deserialize` for convenience but custom parsing logic is in `from_value`. 15 | /// Does not derive `Serialize` as a custom implementation is needed to add `jsonrpc: "2.0"`. 16 | #[derive(Deserialize, Debug, Clone, PartialEq)] 17 | pub struct RpcNotification { 18 | pub method: String, 19 | pub params: Option, 20 | } 21 | 22 | impl RpcNotification { 23 | /// Parses a `serde_json::Value` into an `RpcNotification`. 24 | /// 25 | /// This method performs checks according to the JSON-RPC 2.0 specification: 26 | /// 1. Checks if the input is a JSON object. 27 | /// 2. Validates the presence and value of the `"jsonrpc": "2.0"` field. 28 | /// 3. Validates the presence and type of the `"method"` field (must be a string). 29 | /// 4. Validates the type of the `"params"` field (must be array or object if present). 30 | /// 5. Ensures no `"id"` field is present (as it's a notification). 31 | /// 32 | /// # Errors 33 | /// Returns `RpcRequestParsingError` if any validation fails. 34 | pub fn from_value(value: Value) -> Result { 35 | let value_type = get_json_type(&value); 36 | 37 | let Value::Object(mut obj) = value else { 38 | return Err(RpcRequestParsingError::RequestInvalidType { 39 | actual_type: value_type.to_string(), 40 | }); 41 | }; 42 | 43 | // -- Check Version 44 | let version_val = extract_value(&mut obj, "jsonrpc"); 45 | if let Err(version_result) = validate_version(version_val) { 46 | // Attempt to get method for better error context (id is None for notifications) 47 | let method_val = extract_value(&mut obj, "method"); 48 | let method = method_val.and_then(|v| v.as_str().map(|s| s.to_string())); 49 | return match version_result { 50 | Some(v) => Err(RpcRequestParsingError::VersionInvalid { 51 | id: None, // Notifications have no ID 52 | method, 53 | version: v, 54 | }), 55 | None => Err(RpcRequestParsingError::VersionMissing { 56 | id: None, // Notifications have no ID 57 | method, 58 | }), 59 | }; 60 | } 61 | 62 | // -- Check Method 63 | let method_val = extract_value(&mut obj, "method"); 64 | let method = match parse_method(method_val) { 65 | Ok(m) => m, 66 | Err(method_result) => { 67 | return match method_result { 68 | Some(m) => Err(RpcRequestParsingError::MethodInvalidType { 69 | id: None, // Notifications have no ID 70 | method: m, 71 | }), 72 | None => Err(RpcRequestParsingError::MethodMissing { id: None }), // Notifications have no ID 73 | }; 74 | } 75 | }; 76 | 77 | // -- Check Params 78 | let params_val = extract_value(&mut obj, "params"); 79 | let params = parse_params(params_val)?; // Propagates ParamsInvalidType error 80 | 81 | // -- Check for disallowed ID field 82 | if let Some(id_val) = extract_value(&mut obj, "id") { 83 | return Err(RpcRequestParsingError::NotificationHasId { 84 | method: Some(method), 85 | id: id_val, 86 | }); 87 | } 88 | 89 | Ok(RpcNotification { method, params }) 90 | } 91 | } 92 | 93 | // region: --- Serialize Custom 94 | 95 | impl Serialize for RpcNotification { 96 | fn serialize(&self, serializer: S) -> Result 97 | where 98 | S: Serializer, 99 | { 100 | // Determine the number of fields: jsonrpc, method are always present. params is optional. 101 | let mut field_count = 2; 102 | if self.params.is_some() { 103 | field_count += 1; 104 | } 105 | 106 | let mut state = serializer.serialize_struct("RpcNotification", field_count)?; 107 | 108 | // Always add "jsonrpc": "2.0" 109 | state.serialize_field("jsonrpc", "2.0")?; 110 | 111 | state.serialize_field("method", &self.method)?; 112 | 113 | // Serialize params only if it's Some 114 | if let Some(params) = &self.params { 115 | state.serialize_field("params", params)?; 116 | } 117 | 118 | state.end() 119 | } 120 | } 121 | 122 | // endregion: --- Serialize Custom 123 | 124 | // region: --- Froms 125 | 126 | impl From for RpcNotification { 127 | fn from(request: RpcRequest) -> Self { 128 | RpcNotification { 129 | method: request.method, 130 | params: request.params, 131 | } 132 | } 133 | } 134 | 135 | // endregion: --- Froms 136 | 137 | // region: --- TryFrom 138 | 139 | /// Convenient TryFrom, performs strict JSON-RPC 2.0 validation via `RpcNotification::from_value`. 140 | impl TryFrom for RpcNotification { 141 | type Error = RpcRequestParsingError; 142 | fn try_from(value: Value) -> Result { 143 | RpcNotification::from_value(value) 144 | } 145 | } 146 | 147 | // endregion: --- TryFrom 148 | 149 | // region: --- Tests 150 | 151 | #[cfg(test)] 152 | mod tests { 153 | use super::*; 154 | use crate::rpc_message::RpcRequestParsingError; 155 | use serde_json::{json, to_value}; 156 | 157 | type Result = core::result::Result>; // For tests. 158 | 159 | // -- Test Data 160 | fn notif_value_ok_params_some() -> Value { 161 | json!({ 162 | "jsonrpc": "2.0", 163 | "method": "updateState", 164 | "params": {"value": 123} 165 | }) 166 | } 167 | 168 | fn notif_value_ok_params_none() -> Value { 169 | json!({ 170 | "jsonrpc": "2.0", 171 | "method": "ping" 172 | }) 173 | } 174 | 175 | fn notif_value_ok_params_arr() -> Value { 176 | json!({ 177 | "jsonrpc": "2.0", 178 | "method": "notifyUsers", 179 | "params": ["user1", "user2"] 180 | }) 181 | } 182 | 183 | fn notif_value_fail_id_present() -> Value { 184 | json!({ 185 | "jsonrpc": "2.0", 186 | "id": 888, // ID is not allowed in notifications 187 | "method": "updateState", 188 | "params": {"value": 123} 189 | }) 190 | } 191 | 192 | fn notif_value_fail_version_missing() -> Value { 193 | json!({ 194 | // "jsonrpc": "2.0", // Missing 195 | "method": "updateState" 196 | }) 197 | } 198 | 199 | fn notif_value_fail_version_invalid() -> Value { 200 | json!({ 201 | "jsonrpc": "1.0", // Invalid 202 | "method": "updateState" 203 | }) 204 | } 205 | 206 | fn notif_value_fail_method_missing() -> Value { 207 | json!({ 208 | "jsonrpc": "2.0" 209 | // "method": "updateState" // Missing 210 | }) 211 | } 212 | 213 | fn notif_value_fail_method_invalid() -> Value { 214 | json!({ 215 | "jsonrpc": "2.0", 216 | "method": 123 // Invalid type 217 | }) 218 | } 219 | 220 | fn notif_value_fail_params_invalid() -> Value { 221 | json!({ 222 | "jsonrpc": "2.0", 223 | "method": "update", 224 | "params": "not-array-or-object" // Invalid type 225 | }) 226 | } 227 | 228 | // region: --- Serialize Tests 229 | #[test] 230 | fn test_rpc_notification_serialize_ok_params_some() -> Result<()> { 231 | // -- Setup & Fixtures 232 | let notif = RpcNotification { 233 | method: "updateState".to_string(), 234 | params: Some(json!({"value": 123})), 235 | }; 236 | 237 | // -- Exec 238 | let value = to_value(notif)?; 239 | 240 | // -- Check 241 | assert_eq!(value, notif_value_ok_params_some()); 242 | Ok(()) 243 | } 244 | 245 | #[test] 246 | fn test_rpc_notification_serialize_ok_params_none() -> Result<()> { 247 | // -- Setup & Fixtures 248 | let notif = RpcNotification { 249 | method: "ping".to_string(), 250 | params: None, 251 | }; 252 | 253 | // -- Exec 254 | let value = to_value(notif)?; 255 | 256 | // -- Check 257 | assert_eq!(value, notif_value_ok_params_none()); 258 | Ok(()) 259 | } 260 | 261 | #[test] 262 | fn test_rpc_notification_serialize_ok_params_arr() -> Result<()> { 263 | // -- Setup & Fixtures 264 | let notif = RpcNotification { 265 | method: "notifyUsers".to_string(), 266 | params: Some(json!(["user1", "user2"])), 267 | }; 268 | 269 | // -- Exec 270 | let value = to_value(notif)?; 271 | 272 | // -- Check 273 | assert_eq!(value, notif_value_ok_params_arr()); 274 | Ok(()) 275 | } 276 | // endregion: --- Serialize Tests 277 | 278 | // region: --- Deserialize (from_value) Tests 279 | #[test] 280 | fn test_rpc_notification_from_value_ok_params_some() -> Result<()> { 281 | // -- Setup & Fixtures 282 | let value = notif_value_ok_params_some(); 283 | let expected = RpcNotification { 284 | method: "updateState".to_string(), 285 | params: Some(json!({"value": 123})), 286 | }; 287 | 288 | // -- Exec 289 | let notification = RpcNotification::from_value(value)?; 290 | 291 | // -- Check 292 | assert_eq!(notification, expected); 293 | Ok(()) 294 | } 295 | 296 | #[test] 297 | fn test_rpc_notification_from_value_ok_params_none() -> Result<()> { 298 | // -- Setup & Fixtures 299 | let value = notif_value_ok_params_none(); 300 | let expected = RpcNotification { 301 | method: "ping".to_string(), 302 | params: None, 303 | }; 304 | 305 | // -- Exec 306 | let notification = RpcNotification::from_value(value)?; 307 | 308 | // -- Check 309 | assert_eq!(notification, expected); 310 | Ok(()) 311 | } 312 | 313 | #[test] 314 | fn test_rpc_notification_from_value_ok_params_arr() -> Result<()> { 315 | // -- Setup & Fixtures 316 | let value = notif_value_ok_params_arr(); 317 | let expected = RpcNotification { 318 | method: "notifyUsers".to_string(), 319 | params: Some(json!(["user1", "user2"])), 320 | }; 321 | 322 | // -- Exec 323 | let notification = RpcNotification::from_value(value)?; 324 | 325 | // -- Check 326 | assert_eq!(notification, expected); 327 | Ok(()) 328 | } 329 | 330 | #[test] 331 | fn test_rpc_notification_from_value_fail_id_present() -> Result<()> { 332 | // -- Setup & Fixtures 333 | let value = notif_value_fail_id_present(); 334 | 335 | // -- Exec 336 | let result = RpcNotification::from_value(value); 337 | 338 | // -- Check 339 | assert!(matches!( 340 | result, 341 | Err(RpcRequestParsingError::NotificationHasId { method: Some(_), id: _ }) 342 | )); 343 | if let Err(RpcRequestParsingError::NotificationHasId { method, id }) = result { 344 | assert_eq!(method.unwrap(), "updateState"); 345 | assert_eq!(id, json!(888)); 346 | } else { 347 | panic!("Expected NotificationHasId error"); 348 | } 349 | Ok(()) 350 | } 351 | 352 | #[test] 353 | fn test_rpc_notification_from_value_fail_version_missing() -> Result<()> { 354 | // -- Setup & Fixtures 355 | let value = notif_value_fail_version_missing(); 356 | 357 | // -- Exec 358 | let result = RpcNotification::from_value(value); 359 | 360 | // -- Check 361 | assert!(matches!( 362 | result, 363 | Err(RpcRequestParsingError::VersionMissing { 364 | id: None, 365 | method: Some(_) 366 | }) 367 | )); 368 | if let Err(RpcRequestParsingError::VersionMissing { id, method }) = result { 369 | assert!(id.is_none()); 370 | assert_eq!(method.unwrap(), "updateState"); 371 | } else { 372 | panic!("Expected VersionMissing error"); 373 | } 374 | Ok(()) 375 | } 376 | 377 | #[test] 378 | fn test_rpc_notification_from_value_fail_version_invalid() -> Result<()> { 379 | // -- Setup & Fixtures 380 | let value = notif_value_fail_version_invalid(); 381 | 382 | // -- Exec 383 | let result = RpcNotification::from_value(value); 384 | 385 | // -- Check 386 | assert!(matches!( 387 | result, 388 | Err(RpcRequestParsingError::VersionInvalid { 389 | id: None, 390 | method: Some(_), 391 | version: _ 392 | }) 393 | )); 394 | if let Err(RpcRequestParsingError::VersionInvalid { id, method, version }) = result { 395 | assert!(id.is_none()); 396 | assert_eq!(method.unwrap(), "updateState"); 397 | assert_eq!(version, json!("1.0")); 398 | } else { 399 | panic!("Expected VersionInvalid error"); 400 | } 401 | Ok(()) 402 | } 403 | 404 | #[test] 405 | fn test_rpc_notification_from_value_fail_method_missing() -> Result<()> { 406 | // -- Setup & Fixtures 407 | let value = notif_value_fail_method_missing(); 408 | 409 | // -- Exec 410 | let result = RpcNotification::from_value(value); 411 | 412 | // -- Check 413 | assert!(matches!( 414 | result, 415 | Err(RpcRequestParsingError::MethodMissing { id: None }) 416 | )); 417 | Ok(()) 418 | } 419 | 420 | #[test] 421 | fn test_rpc_notification_from_value_fail_method_invalid() -> Result<()> { 422 | // -- Setup & Fixtures 423 | let value = notif_value_fail_method_invalid(); 424 | 425 | // -- Exec 426 | let result = RpcNotification::from_value(value); 427 | 428 | // -- Check 429 | assert!(matches!( 430 | result, 431 | Err(RpcRequestParsingError::MethodInvalidType { id: None, method: _ }) 432 | )); 433 | if let Err(RpcRequestParsingError::MethodInvalidType { id, method }) = result { 434 | assert!(id.is_none()); 435 | assert_eq!(method, json!(123)); 436 | } else { 437 | panic!("Expected MethodInvalidType error"); 438 | } 439 | Ok(()) 440 | } 441 | 442 | #[test] 443 | fn test_rpc_notification_from_value_fail_params_invalid() -> Result<()> { 444 | // -- Setup & Fixtures 445 | let value = notif_value_fail_params_invalid(); 446 | 447 | // -- Exec 448 | let result = RpcNotification::from_value(value); 449 | 450 | // -- Check 451 | assert!(matches!( 452 | result, 453 | Err(RpcRequestParsingError::ParamsInvalidType { actual_type: _ }) 454 | )); 455 | if let Err(RpcRequestParsingError::ParamsInvalidType { actual_type }) = result { 456 | assert_eq!(actual_type, "String"); 457 | } else { 458 | panic!("Expected ParamsInvalidType error"); 459 | } 460 | Ok(()) 461 | } 462 | 463 | #[test] 464 | fn test_rpc_notification_from_value_fail_not_object() -> Result<()> { 465 | // -- Setup & Fixtures 466 | let value = json!("not an object"); 467 | 468 | // -- Exec 469 | let result = RpcNotification::from_value(value); 470 | 471 | // -- Check 472 | assert!(matches!( 473 | result, 474 | Err(RpcRequestParsingError::RequestInvalidType { actual_type: _ }) 475 | )); 476 | if let Err(RpcRequestParsingError::RequestInvalidType { actual_type }) = result { 477 | assert_eq!(actual_type, "String"); 478 | } else { 479 | panic!("Expected RequestInvalidType error"); 480 | } 481 | Ok(()) 482 | } 483 | // endregion: --- Deserialize (from_value) Tests 484 | } 485 | // endregion: --- Tests 486 | -------------------------------------------------------------------------------- /src/rpc_response/response.rs: -------------------------------------------------------------------------------- 1 | use crate::RpcId; 2 | use crate::router::{CallError, CallResult, CallSuccess}; 3 | use crate::rpc_response::{RpcError, RpcResponseParsingError}; 4 | use serde::de::{MapAccess, Visitor}; 5 | use serde::ser::SerializeMap; 6 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 7 | use serde_json::Value; 8 | use std::fmt; 9 | 10 | /// Represents a JSON-RPC 2.0 Response object. 11 | /// It can be either a success or an error response. 12 | /// 13 | #[derive(Debug, Clone, PartialEq)] 14 | pub enum RpcResponse { 15 | /// Represents a successful JSON-RPC response. 16 | Success(RpcSuccessResponse), 17 | /// Represents a JSON-RPC error response. 18 | Error(RpcErrorResponse), 19 | } 20 | 21 | /// Holds the components of a successful JSON-RPC response. 22 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 23 | pub struct RpcSuccessResponse { 24 | /// The request ID this response corresponds to. 25 | pub id: RpcId, 26 | 27 | /// The result payload of the successful RPC call. 28 | pub result: Value, 29 | } 30 | 31 | /// Holds the components of a JSON-RPC error response. 32 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 33 | pub struct RpcErrorResponse { 34 | /// The request ID this response corresponds to. Can be `Null` if the request ID couldn't be determined. 35 | pub id: RpcId, 36 | 37 | /// The error object containing details about the failure. 38 | pub error: RpcError, 39 | } 40 | 41 | // region: --- Constructors 42 | 43 | impl RpcResponse { 44 | pub fn from_success(id: RpcId, result: Value) -> Self { 45 | Self::Success(RpcSuccessResponse { id, result }) 46 | } 47 | 48 | pub fn from_error(id: RpcId, error: RpcError) -> Self { 49 | Self::Error(RpcErrorResponse { id, error }) 50 | } 51 | } 52 | 53 | // endregion: --- Constructors 54 | 55 | // region: --- Accessors 56 | 57 | impl RpcResponse { 58 | pub fn is_success(&self) -> bool { 59 | matches!(self, RpcResponse::Success(_)) 60 | } 61 | 62 | pub fn is_error(&self) -> bool { 63 | matches!(self, RpcResponse::Error(_)) 64 | } 65 | 66 | pub fn id(&self) -> &RpcId { 67 | match self { 68 | RpcResponse::Success(r) => &r.id, 69 | RpcResponse::Error(r) => &r.id, 70 | } 71 | } 72 | 73 | /// Consumes the response and returns its parts: the ID and a `Result` containing 74 | /// either the success value or the error object. 75 | pub fn into_parts(self) -> (RpcId, core::result::Result) { 76 | match self { 77 | RpcResponse::Success(r) => (r.id, Ok(r.result)), 78 | RpcResponse::Error(r) => (r.id, Err(r.error)), 79 | } 80 | } 81 | } 82 | // endregion: --- Accessors 83 | 84 | // region: --- From Router CallResult/CallSuccess/CallError 85 | 86 | impl From for RpcResponse { 87 | /// Converts a successful router `CallSuccess` into a JSON-RPC `RpcResponse::Success`. 88 | fn from(call_success: CallSuccess) -> Self { 89 | RpcResponse::from_success(call_success.id, call_success.value) 90 | } 91 | } 92 | 93 | impl From for RpcResponse { 94 | /// Converts a router `CallError` into a JSON-RPC `RpcResponse::Error`. 95 | fn from(call_error: CallError) -> Self { 96 | let id = call_error.id.clone(); // Clone id before moving call_error 97 | let error = RpcError::from(call_error); // Reuse From for RpcError 98 | RpcResponse::from_error(id, error) 99 | } 100 | } 101 | 102 | impl From for RpcResponse { 103 | /// Converts a router `CallResult` (which is Result) 104 | /// into the appropriate JSON-RPC `RpcResponse`. 105 | fn from(call_result: CallResult) -> Self { 106 | match call_result { 107 | Ok(call_success) => RpcResponse::from(call_success), 108 | Err(call_error) => RpcResponse::from(call_error), 109 | } 110 | } 111 | } 112 | 113 | // endregion: --- From Router CallResult/CallSuccess/CallError 114 | 115 | // region: --- Serde Impls 116 | 117 | impl Serialize for RpcResponse { 118 | fn serialize(&self, serializer: S) -> Result 119 | where 120 | S: Serializer, 121 | { 122 | let mut map = serializer.serialize_map(Some(3))?; 123 | map.serialize_entry("jsonrpc", "2.0")?; 124 | 125 | match self { 126 | RpcResponse::Success(RpcSuccessResponse { id, result }) => { 127 | map.serialize_entry("id", id)?; 128 | map.serialize_entry("result", result)?; 129 | } 130 | RpcResponse::Error(RpcErrorResponse { id, error }) => { 131 | map.serialize_entry("id", id)?; 132 | map.serialize_entry("error", error)?; 133 | } 134 | } 135 | 136 | map.end() 137 | } 138 | } 139 | 140 | impl<'de> Deserialize<'de> for RpcResponse { 141 | fn deserialize(deserializer: D) -> Result 142 | where 143 | D: Deserializer<'de>, 144 | { 145 | struct RpcResponseVisitor; 146 | 147 | impl<'de> Visitor<'de> for RpcResponseVisitor { 148 | type Value = RpcResponse; 149 | 150 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 151 | formatter.write_str("a JSON-RPC 2.0 response object") 152 | } 153 | 154 | fn visit_map(self, mut map: A) -> Result 155 | where 156 | A: MapAccess<'de>, 157 | { 158 | let mut version: Option = None; 159 | let mut id_val: Option = None; 160 | let mut result_val: Option = None; 161 | let mut error_val: Option = None; 162 | 163 | while let Some(key) = map.next_key::()? { 164 | match key.as_str() { 165 | "jsonrpc" => { 166 | if version.is_some() { 167 | return Err(serde::de::Error::duplicate_field("jsonrpc")); 168 | } 169 | version = Some(map.next_value()?); 170 | } 171 | "id" => { 172 | if id_val.is_some() { 173 | return Err(serde::de::Error::duplicate_field("id")); 174 | } 175 | // Deserialize as Value first to handle String/Number/Null correctly 176 | id_val = Some(map.next_value()?); 177 | } 178 | "result" => { 179 | if result_val.is_some() { 180 | return Err(serde::de::Error::duplicate_field("result")); 181 | } 182 | result_val = Some(map.next_value()?); 183 | } 184 | "error" => { 185 | if error_val.is_some() { 186 | return Err(serde::de::Error::duplicate_field("error")); 187 | } 188 | // Deserialize error as Value for now, parse fully later 189 | error_val = Some(map.next_value()?); 190 | } 191 | _ => { 192 | // Ignore unknown fields 193 | let _: Value = map.next_value()?; 194 | } 195 | } 196 | } 197 | 198 | // Validate jsonrpc version 199 | let id_for_error = id_val.as_ref().and_then(|v| RpcId::from_value(v.clone()).ok()); 200 | match version.as_deref() { 201 | Some("2.0") => {} // OK 202 | Some(v) => { 203 | return Err(serde::de::Error::custom( 204 | RpcResponseParsingError::InvalidJsonRpcVersion { 205 | id: id_for_error, 206 | expected: "2.0", 207 | actual: Some(Value::String(v.to_string())), 208 | }, 209 | )); 210 | } 211 | None => { 212 | return Err(serde::de::Error::custom( 213 | RpcResponseParsingError::MissingJsonRpcVersion { id: id_for_error }, 214 | )); 215 | } 216 | }; 217 | 218 | // Parse id 219 | let id = match id_val { 220 | Some(v) => RpcId::from_value(v) 221 | .map_err(|e| serde::de::Error::custom(RpcResponseParsingError::InvalidId(e)))?, 222 | None => return Err(serde::de::Error::custom(RpcResponseParsingError::MissingId)), 223 | }; 224 | 225 | // Determine if Success or Error 226 | match (result_val, error_val) { 227 | (Some(result), None) => Ok(RpcResponse::Success(RpcSuccessResponse { id, result })), 228 | (None, Some(error_value)) => { 229 | // Now parse the error object from the Value 230 | let error: RpcError = serde_json::from_value(error_value) 231 | .map_err(|e| serde::de::Error::custom(RpcResponseParsingError::InvalidErrorObject(e)))?; 232 | Ok(RpcResponse::Error(RpcErrorResponse { id, error })) 233 | } 234 | (Some(_), Some(_)) => Err(serde::de::Error::custom(RpcResponseParsingError::BothResultAndError { 235 | id: id.clone(), 236 | })), 237 | (None, None) => Err(serde::de::Error::custom( 238 | RpcResponseParsingError::MissingResultAndError { id: id.clone() }, 239 | )), 240 | } 241 | } 242 | } 243 | 244 | deserializer.deserialize_map(RpcResponseVisitor) 245 | } 246 | } 247 | 248 | // endregion: --- Serde Impls 249 | 250 | // region: --- Tests 251 | 252 | #[cfg(test)] 253 | mod tests { 254 | use super::*; 255 | use crate::Error as RouterError; // Import router Error 256 | use serde_json::{from_value, json, to_value}; 257 | 258 | type TestResult = core::result::Result>; // For tests. 259 | 260 | // Helper to create CallError easily 261 | fn create_call_error(id: impl Into, method: &str, error: RouterError) -> CallError { 262 | CallError { 263 | id: id.into(), 264 | method: method.to_string(), 265 | error, 266 | } 267 | } 268 | 269 | #[test] 270 | fn test_rpc_response_success_ser_de() -> TestResult<()> { 271 | // -- Setup & Fixtures 272 | let id = RpcId::Number(1); 273 | let result_val = json!({"data": "ok"}); 274 | let response = RpcResponse::from_success(id.clone(), result_val.clone()); 275 | let expected_json = json!({ 276 | "jsonrpc": "2.0", 277 | "id": 1, 278 | "result": {"data": "ok"} 279 | }); 280 | 281 | // -- Exec: Serialize 282 | let serialized_value = to_value(&response)?; 283 | 284 | // -- Check: Serialize 285 | assert_eq!(serialized_value, expected_json); 286 | 287 | // -- Exec: Deserialize 288 | let deserialized_response: RpcResponse = from_value(serialized_value)?; 289 | 290 | // -- Check: Deserialize 291 | assert_eq!(deserialized_response, response); 292 | assert_eq!(deserialized_response.id(), &id); 293 | assert!(deserialized_response.is_success()); 294 | assert!(!deserialized_response.is_error()); 295 | let (resp_id, resp_result) = deserialized_response.into_parts(); 296 | assert_eq!(resp_id, id); 297 | assert_eq!(resp_result.unwrap(), result_val); 298 | 299 | Ok(()) 300 | } 301 | 302 | #[test] 303 | fn test_rpc_response_error_ser_de() -> TestResult<()> { 304 | // -- Setup & Fixtures 305 | let id = RpcId::String("req-abc".into()); 306 | let rpc_error = RpcError { 307 | code: -32601, 308 | message: "Method not found".to_string(), 309 | data: Some(json!("method_name")), 310 | }; 311 | let response = RpcResponse::from_error(id.clone(), rpc_error.clone()); 312 | let expected_json = json!({ 313 | "jsonrpc": "2.0", 314 | "id": "req-abc", 315 | "error": { 316 | "code": -32601, 317 | "message": "Method not found", 318 | "data": "method_name" 319 | } 320 | }); 321 | 322 | // -- Exec: Serialize 323 | let serialized_value = to_value(&response)?; 324 | 325 | // -- Check: Serialize 326 | assert_eq!(serialized_value, expected_json); 327 | 328 | // -- Exec: Deserialize 329 | let deserialized_response: RpcResponse = from_value(serialized_value)?; 330 | 331 | // -- Check: Deserialize 332 | assert_eq!(deserialized_response, response); 333 | assert_eq!(deserialized_response.id(), &id); 334 | assert!(!deserialized_response.is_success()); 335 | assert!(deserialized_response.is_error()); 336 | let (resp_id, resp_result) = deserialized_response.into_parts(); 337 | assert_eq!(resp_id, id); 338 | assert_eq!(resp_result.unwrap_err(), rpc_error); 339 | 340 | Ok(()) 341 | } 342 | 343 | #[test] 344 | fn test_rpc_response_error_ser_de_no_data() -> TestResult<()> { 345 | // -- Setup & Fixtures 346 | let id = RpcId::Null; 347 | let rpc_error = RpcError { 348 | code: -32700, 349 | message: "Parse error".to_string(), 350 | data: None, // No data 351 | }; 352 | let response = RpcResponse::from_error(id.clone(), rpc_error.clone()); 353 | let expected_json = json!({ 354 | "jsonrpc": "2.0", 355 | "id": null, 356 | "error": { 357 | "code": -32700, 358 | "message": "Parse error" 359 | // "data" field is omitted 360 | } 361 | }); 362 | 363 | // -- Exec: Serialize 364 | let serialized_value = to_value(&response)?; 365 | 366 | // -- Check: Serialize 367 | assert_eq!(serialized_value, expected_json); 368 | 369 | // -- Exec: Deserialize 370 | let deserialized_response: RpcResponse = from_value(serialized_value)?; 371 | 372 | // -- Check: Deserialize 373 | assert_eq!(deserialized_response, response); 374 | assert_eq!(deserialized_response.id(), &id); 375 | assert!(deserialized_response.is_error()); 376 | let (resp_id, resp_result) = deserialized_response.into_parts(); 377 | assert_eq!(resp_id, id); 378 | assert_eq!(resp_result.unwrap_err(), rpc_error); 379 | 380 | Ok(()) 381 | } 382 | 383 | #[test] 384 | fn test_rpc_response_de_invalid() { 385 | // -- Setup & Fixtures 386 | let invalid_jsons = vec![ 387 | // Missing jsonrpc 388 | json!({"id": 1, "result": "ok"}), 389 | // Invalid jsonrpc version 390 | json!({"jsonrpc": "1.0", "id": 1, "result": "ok"}), 391 | // Missing id 392 | json!({"jsonrpc": "2.0", "result": "ok"}), 393 | // Missing result and error 394 | json!({"jsonrpc": "2.0", "id": 1}), 395 | // Both result and error 396 | json!({"jsonrpc": "2.0", "id": 1, "result": "ok", "error": {"code": 1, "message": "err"}}), 397 | // Invalid error object (e.g., wrong type) 398 | json!({"jsonrpc": "2.0", "id": 1, "error": "not an object"}), 399 | // Invalid error object (missing code) 400 | json!({"jsonrpc": "2.0", "id": 1, "error": {"message": "err"}}), 401 | // Invalid error object (missing message) 402 | json!({"jsonrpc": "2.0", "id": 1, "error": {"code": 1}}), 403 | // Invalid id type 404 | json!({"jsonrpc": "2.0", "id": [1,2], "result": "ok"}), 405 | ]; 406 | 407 | // -- Exec & Check 408 | for json_value in invalid_jsons { 409 | let result: Result = from_value(json_value.clone()); 410 | assert!(result.is_err(), "Expected error for invalid JSON: {}", json_value); 411 | } 412 | } 413 | 414 | // region: --- From Router Call Tests 415 | #[test] 416 | fn test_from_call_success() -> TestResult<()> { 417 | // -- Setup & Fixtures 418 | let call_success = CallSuccess { 419 | id: RpcId::Number(101), 420 | method: "test_method".to_string(), 421 | value: json!({"success": true}), 422 | }; 423 | 424 | // -- Exec 425 | let rpc_response = RpcResponse::from(call_success); 426 | 427 | // -- Check 428 | match rpc_response { 429 | RpcResponse::Success(RpcSuccessResponse { id, result }) => { 430 | assert_eq!(id, RpcId::Number(101)); 431 | assert_eq!(result, json!({"success": true})); 432 | } 433 | RpcResponse::Error(_) => panic!("Expected RpcResponse::Success"), 434 | } 435 | Ok(()) 436 | } 437 | 438 | #[test] 439 | fn test_from_call_error() -> TestResult<()> { 440 | // -- Setup & Fixtures 441 | let call_error = create_call_error(102, "test_method", RouterError::MethodUnknown); 442 | 443 | // -- Exec 444 | let rpc_response = RpcResponse::from(call_error); 445 | 446 | // -- Check 447 | match rpc_response { 448 | RpcResponse::Error(RpcErrorResponse { id, error }) => { 449 | assert_eq!(id, RpcId::Number(102)); 450 | assert_eq!(error.code, RpcError::CODE_METHOD_NOT_FOUND); 451 | assert_eq!(error.message, "Method not found"); 452 | assert!(error.data.is_some()); // contains RouterError::MethodUnknown display 453 | } 454 | RpcResponse::Success(_) => panic!("Expected RpcResponse::Error"), 455 | } 456 | Ok(()) 457 | } 458 | 459 | #[test] 460 | fn test_from_call_result_ok() -> TestResult<()> { 461 | // -- Setup & Fixtures 462 | let call_result: CallResult = Ok(CallSuccess { 463 | id: 103.into(), 464 | method: "test_method".to_string(), 465 | value: json!("ok_data"), 466 | }); 467 | 468 | // -- Exec 469 | let rpc_response = RpcResponse::from(call_result); 470 | 471 | // -- Check 472 | match rpc_response { 473 | RpcResponse::Success(RpcSuccessResponse { id, result }) => { 474 | assert_eq!(id, RpcId::Number(103)); 475 | assert_eq!(result, json!("ok_data")); 476 | } 477 | RpcResponse::Error(_) => panic!("Expected RpcResponse::Success"), 478 | } 479 | Ok(()) 480 | } 481 | 482 | #[test] 483 | fn test_from_call_result_err() -> TestResult<()> { 484 | // -- Setup & Fixtures 485 | let call_result: CallResult = Err(create_call_error( 486 | "err-104", 487 | "test_method", 488 | RouterError::ParamsMissingButRequested, 489 | )); 490 | 491 | // -- Exec 492 | let rpc_response = RpcResponse::from(call_result); 493 | 494 | // -- Check 495 | match rpc_response { 496 | RpcResponse::Error(RpcErrorResponse { id, error }) => { 497 | assert_eq!(id, RpcId::String("err-104".into())); 498 | assert_eq!(error.code, RpcError::CODE_INVALID_PARAMS); 499 | assert_eq!(error.message, "Invalid params"); 500 | assert!(error.data.is_some()); // contains RouterError::ParamsMissingButRequested display 501 | } 502 | RpcResponse::Success(_) => panic!("Expected RpcResponse::Error"), 503 | } 504 | Ok(()) 505 | } 506 | // endregion: --- From Router Call Tests 507 | } 508 | 509 | // endregion: --- Tests 510 | -------------------------------------------------------------------------------- /doc/all-apis.md: -------------------------------------------------------------------------------- 1 | # RPC Router - All APIs 2 | 3 | This document provides a comprehensive overview of all public APIs exposed by the `rpc-router` crate. 4 | 5 | ## Core Types 6 | 7 | - **`Router`**: The central component that holds RPC method routes and associated handlers. It's typically created using `RouterBuilder` and wrapped in an `Arc` for efficient sharing. 8 | - **`RouterBuilder`**: Used to configure and build a `Router`. Allows adding handlers and base resources. 9 | - **`Resources`**: A type map used to hold shared application state or resources (like database connections, configuration, etc.) accessible by handlers. 10 | - **`ResourcesBuilder`**: Used to configure and build `Resources`. 11 | 12 | ## Request Handling Flow 13 | 14 | 1. **Parsing the Message**: Incoming JSON messages are parsed. 15 | - If the message contains an `id`, it's parsed into an `RpcRequest`. 16 | - If the message lacks an `id`, it's parsed into an `RpcNotification`. 17 | - Parsing involves strict validation against the JSON-RPC 2.0 specification (e.g., `jsonrpc: "2.0"`, method format, params format). 18 | 2. **Calling the Router (for Requests)**: The `Router::call` or `Router::call_with_resources` method is invoked with the `RpcRequest` and optional additional `Resources`. 19 | 3. **Method Routing**: The router finds the handler registered for the requested `method`. 20 | 4. **Resource Injection**: The router attempts to extract required resources (types implementing `FromResources`) from the provided `Resources`. 21 | 5. **Parameter Deserialization**: If the handler expects parameters, the `params` field of the `RpcRequest` (an `Option`) is deserialized into the expected type using the `IntoParams` trait. 22 | 6. **Handler Execution**: The asynchronous handler function is called with the injected resources and deserialized parameters. 23 | 7. **Result Handling**: The handler returns a `HandlerResult` (which is `Result`). 24 | 8. **Router Response**: The router captures the handler's result or any errors during resource/parameter handling and wraps it in a `CallResult` (`Result`). 25 | 9. **JSON-RPC Response Formation**: The `CallResult` is typically converted into a standard JSON-RPC `RpcResponse` (containing either `RpcSuccessResponse` or `RpcErrorResponse`) for sending back to the client. This conversion handles mapping internal router errors (`router::Error`) to standard `RpcError` objects. 26 | 10. **Handling Notifications**: Notifications (`RpcNotification`) are typically processed separately from requests, as they do not produce a response. The application logic decides how to handle them (e.g., logging, triggering background tasks). The `Router` itself is primarily designed for request-response interactions. 27 | 28 | ## JSON-RPC Message Parsing 29 | 30 | ### `RpcRequest` (Request with ID) 31 | 32 | - **`RpcRequest`**: Represents a parsed JSON-RPC request that expects a response. 33 | ```rust 34 | pub struct RpcRequest { 35 | pub id: RpcId, 36 | pub method: String, 37 | pub params: Option, // Must be Object or Array if Some 38 | } 39 | ``` 40 | - **`RpcId`**: An enum representing the JSON-RPC ID (`String(Arc)`, `Number(i64)`, or `Null`). See `RpcId` section below for details. 41 | - **Parsing**: 42 | - **`RpcRequest::from_value(value: Value) -> Result`**: Parses a `serde_json::Value` into an `RpcRequest`. Performs strict validation (`RpcRequestCheckFlags::ALL`: `VERSION` and `ID`). 43 | - **`RpcRequest::from_value_with_checks(value: Value, checks: RpcRequestCheckFlags) -> Result`**: Parses with customizable validation. 44 | - `RpcRequestCheckFlags`: Bitflags to control checks: 45 | - `VERSION`: Checks for `"jsonrpc": "2.0"`. 46 | - `ID`: Checks for presence and validity (String, Number, *not* Null) of the `id` field. If flag is *not* set, defaults to `RpcId::Null` if `id` is missing or invalid. 47 | - `ALL`: Enables `VERSION` and `ID`. 48 | - Example (skip ID check, allowing missing ID): 49 | ```rust 50 | use rpc_router::{RpcRequest, RpcRequestCheckFlags, RpcRequestParsingError, RpcId}; 51 | use serde_json::json; 52 | 53 | let request_value = json!({ 54 | "jsonrpc": "2.0", 55 | // "id": 123, // ID missing, but we'll skip the check 56 | "method": "my_method", 57 | "params": [1, 2] 58 | }); 59 | 60 | // Only check version 61 | let flags = RpcRequestCheckFlags::VERSION; 62 | let result = RpcRequest::from_value_with_checks(request_value, flags); 63 | 64 | assert!(result.is_ok()); 65 | let request = result.unwrap(); 66 | // request.id defaults to RpcId::Null when check is skipped and id is missing 67 | assert_eq!(request.id, RpcId::Null); 68 | assert_eq!(request.method, "my_method"); 69 | ``` 70 | - **`TryFrom for RpcRequest`**: Convenience trait implementation calling `RpcRequest::from_value_with_checks(value, RpcRequestCheckFlags::ALL)`. 71 | - **`RpcRequestParsingError`**: Enum detailing specific parsing failures (e.g., `VersionMissing`, `IdMissing`, `MethodInvalidType`, `ParamsInvalidType`). 72 | 73 | ### `RpcNotification` (Request without ID) 74 | 75 | - **`RpcNotification`**: Represents a parsed JSON-RPC notification (request without an `id`). No response is generated for notifications. 76 | ```rust 77 | pub struct RpcNotification { 78 | pub method: String, 79 | pub params: Option, // Must be Object or Array if Some 80 | } 81 | ``` 82 | - **Parsing**: 83 | - **`RpcNotification::from_value(value: Value) -> Result`**: Parses a `serde_json::Value` into an `RpcNotification`. Performs strict validation: checks `"jsonrpc": "2.0"`, presence/type of `method`, type of `params`, and *absence* of `id`. 84 | - **`TryFrom for RpcNotification`**: Convenience trait implementation calling `RpcNotification::from_value`. 85 | - **`RpcRequestParsingError`**: Shared error enum with `RpcRequest`. Relevant variants include `NotificationHasId`, `VersionMissing`, `MethodMissing`, `ParamsInvalidType` etc. 86 | 87 | ### `RpcId` 88 | 89 | - **`RpcId`**: Enum representing the JSON-RPC ID. 90 | ```rust 91 | pub enum RpcId { 92 | String(Arc), 93 | Number(i64), 94 | Null, 95 | } 96 | ``` 97 | - Implements `Serialize`, `Deserialize`, `Clone`, `Debug`, `PartialEq`, `Eq`, `Hash`, `Display`, `Default` (defaults to `Null`). 98 | - **Constructors**: 99 | - `RpcId::from_scheme(kind: IdSchemeKind, enc: IdSchemeEncoding) -> Self`: Generates a new String ID based on a scheme (e.g., UUID v4/v7) and encoding (e.g., standard, Base64, Base58). 100 | - Convenience constructors: `new_uuid_v4()`, `new_uuid_v4_base64()`, `new_uuid_v4_base64url()`, `new_uuid_v4_base58()`, `new_uuid_v7()`, `new_uuid_v7_base64()`, `new_uuid_v7_base64url()`, `new_uuid_v7_base58()`. 101 | - **Conversion**: 102 | - `From`, `From<&str>`, `From`, `From`, `From`. 103 | - `to_value(&self) -> Value`. 104 | - `from_value(value: Value) -> Result`: Parses from `Value`, returning `RpcRequestParsingError::IdInvalid` on failure. 105 | 106 | ## Router Invocation (`Router` Methods) 107 | 108 | - **`Router::call(&self, rpc_request: RpcRequest) -> impl Future`**: Executes the request using the router's base resources. 109 | - **`Router::call_with_resources(&self, rpc_request: RpcRequest, additional_resources: Resources) -> impl Future`**: Executes the request, overlaying `additional_resources` on top of the base resources. Resources are looked up first in `additional_resources`, then in the base resources. 110 | - **`Router::call_route(&self, id: Option, method: impl Into, params: Option) -> impl Future`**: Lower-level call using individual components instead of `RpcRequest`. Uses base resources. `id` defaults to `RpcId::Null` if `None`. 111 | - **`Router::call_route_with_resources(...)`**: Like `call_route` but with `additional_resources`. 112 | 113 | ## Router Call Output (`CallResult`) 114 | 115 | - **`CallResult`**: Type alias for `Result`. This is the return type of the `Router::call...` methods. This is an *intermediate* result used internally and to provide context (like method name) for logging or tracing before creating the final `RpcResponse`. 116 | - **`CallSuccess`**: Struct containing the successful result details. 117 | ```rust 118 | pub struct CallSuccess { 119 | pub id: RpcId, // The ID from the original RpcRequest 120 | pub method: String, // The method from the original RpcRequest 121 | pub value: Value, // Serialized result from the handler 122 | } 123 | ``` 124 | - **`CallError`**: Struct containing error details. 125 | ```rust 126 | pub struct CallError { 127 | pub id: RpcId, // The ID from the original RpcRequest 128 | pub method: String, // The method from the original RpcRequest 129 | pub error: router::Error, // The router/handler error (see Error Handling) 130 | } 131 | ``` 132 | Implements `std::error::Error`, `Display`, `Debug`. 133 | 134 | ## Defining Handlers (`Handler` Trait) 135 | 136 | - **Signature**: Handlers are `async` functions with the following general signature: 137 | ```rust 138 | async fn handler_name( 139 | [resource1: T1, resource2: T2, ...] // 0 or more resources implementing FromResources 140 | [params: P] // 0 or 1 parameter implementing IntoParams 141 | ) -> HandlerResult // R must implement Serialize 142 | where 143 | T1: FromResources, T2: FromResources, ..., 144 | P: IntoParams, 145 | R: Serialize 146 | { 147 | // ... logic ... 148 | Ok(result_value) // or Err(handler_error) 149 | } 150 | ``` 151 | - **`HandlerResult`**: Alias for `Result`. Handlers should return this. 152 | - **`Handler` Trait**: Automatically implemented for functions matching the required signatures (up to 8 resource parameters). You generally don't interact with this trait directly. 153 | - `fn call(self, resources: Resources, params: Option) -> PinFutureValue;` 154 | - `fn into_dyn(self) -> Box;`: Used internally by `RouterBuilder` macros/methods. 155 | 156 | ### Handler Parameters (`IntoParams`) 157 | 158 | - **`IntoParams` Trait**: Implement this for types you want to use as the `params` argument in your handlers. 159 | - Requires `DeserializeOwned + Send`. 160 | - Default `into_params(value: Option) -> Result` method deserializes from `Some(Value)`, returns `Error::ParamsMissingButRequested` if `None`. 161 | - **`IntoDefaultRpcParams` Trait**: Marker trait. If a type implements `IntoDefaultRpcParams` and `Default`, `IntoParams` is automatically implemented such that `T::default()` is used when JSON-RPC params are `null` or absent (`None`). 162 | - **Derive Macro `#[derive(RpcParams)]`** (requires `rpc-router-macros` feature): The recommended way to implement `IntoParams` for simple structs. Equivalent to `impl IntoParams for MyType {}`. 163 | - **Derive Macro `#[derive(RpcParams, Default)]`** (requires `rpc-router-macros` feature): The recommended way to implement `IntoDefaultRpcParams`. Equivalent to `impl IntoDefaultRpcParams for MyType {}` (assuming `Default` is also derived or implemented). 164 | - **Blanket Impls (Optional Features - Use with caution)**: 165 | - `Option`: `IntoParams` is implemented for `Option` where `T: IntoParams`. Allows optional parameters. 166 | - `Value`: `IntoParams` is implemented directly for `serde_json::Value`. Avoids strong typing. 167 | 168 | ### Handler Resources (`FromResources`) 169 | 170 | - **`FromResources` Trait**: Implement this for types you want to inject as resource arguments into your handlers. 171 | - Requires the type to be `Clone + Send + Sync + 'static`. 172 | - Default `from_resources(resources: &Resources) -> FromResourcesResult` method retrieves the type from `Resources`. Returns `FromResourcesError::ResourceNotFound` if not found. 173 | - **Derive Macro `#[derive(RpcResource)]`** (requires `rpc-router-macros` feature): Recommended way. Equivalent to `impl FromResources for MyType {}`. Ensure the type also implements `Clone + Send + Sync + 'static`. 174 | - **Blanket Impl `Option`**: `FromResources` is implemented for `Option` where `T: FromResources`, allowing optional resource injection (returns `Ok(None)` if the resource `T` is not found). 175 | 176 | ## Resources Management (`Resources`, `ResourcesBuilder`) 177 | 178 | - **`Resources`**: A cloneable type map (`Arc`-based internally for cheap cloning). Holds shared state accessible by handlers. 179 | - `Resources::builder() -> ResourcesBuilder` 180 | - `get(&self) -> Option`: Retrieves a clone of the resource. Looks in overlay resources first, then base resources. 181 | - `is_empty(&self) -> bool`: Checks if both base and overlay resources are empty. 182 | - **`ResourcesBuilder`**: Used to construct `Resources`. 183 | - `default()` 184 | - `append(self, val: T) -> Self`: Adds a resource. 185 | - `append_mut(&mut self, val: T)`: Adds a resource without consuming the builder. 186 | - `get(&self) -> Option`: Gets a resource from the builder (before building). 187 | - `build(self) -> Resources`: Creates the `Resources` object (builds the base resources). 188 | - **`resources_builder!` Macro**: Convenience for creating a `ResourcesBuilder` and appending items. 189 | ```rust 190 | use rpc_router::{resources_builder, Resources, FromResources}; 191 | use std::sync::Arc; 192 | 193 | #[derive(Clone)] struct MyDb; 194 | impl FromResources for MyDb {} // Required for Resources 195 | 196 | #[derive(Clone)] struct MyConfig; 197 | impl FromResources for MyConfig {} // Required for Resources 198 | 199 | let resources: Resources = resources_builder!( 200 | Arc::new(MyDb{}), // Resources typically need to be Arc'd or cheap to clone 201 | Arc::new(MyConfig{}) 202 | ).build(); 203 | ``` 204 | 205 | ## Router Configuration (`RouterBuilder`) 206 | 207 | - **`RouterBuilder::default()`**: Creates a new builder. 208 | - **`append(self, name: &'static str, handler: F) -> Self`**: Adds a handler function directly by name. Infers types, convenient but causes monomorphization for each handler signature. 209 | ```rust 210 | use rpc_router::{RouterBuilder, HandlerResult}; 211 | async fn my_async_handler() -> HandlerResult { Ok("hello".to_string()) } 212 | let builder = RouterBuilder::default().append("my_method", my_async_handler); 213 | ``` 214 | - **`append_dyn(self, name: &'static str, dyn_handler: Box) -> Self`**: Adds a type-erased handler. Preferred for dynamic route addition or avoiding monomorphization bloat. Requires manual `into_dyn()` call. 215 | ```rust 216 | use rpc_router::{RouterBuilder, Handler, HandlerResult}; // For .into_dyn() trait method 217 | async fn my_async_handler() -> HandlerResult { Ok("hello".to_string()) } 218 | let builder = RouterBuilder::default().append_dyn("my_method", my_async_handler.into_dyn()); 219 | ``` 220 | - **`append_resource(self, val: T) -> Self`**: Adds a base resource available to all handlers invoked through the resulting `Router`. Requires `T: FromResources + Clone + Send + Sync + 'static`. 221 | - **`extend(self, other_builder: RouterBuilder) -> Self`**: Merges another builder's routes and base resources into this one. Routes/resources from `other_builder` overwrite existing ones with the same name/type. 222 | - **`extend_resources(self, resources_builder: Option) -> Self`**: Adds base resources from another `ResourcesBuilder`. If `self` already has resources, they are merged (new ones overwrite old). 223 | - **`set_resources(self, resources_builder: ResourcesBuilder) -> Self`**: Replaces the router's base resources entirely with those from the provided builder. 224 | - **`build(self) -> Router`**: Builds the final `Router`. The `Router` is cloneable (`Arc`-based). 225 | - **`router_builder!` Macro**: Convenience macro for building a `RouterBuilder`. 226 | ```rust 227 | use rpc_router::{router_builder, Router, Handler, HandlerError, FromResources}; 228 | 229 | async fn handler_one() -> Result { Ok("one".to_string()) } 230 | async fn handler_two() -> Result { Ok("two".to_string()) } 231 | 232 | #[derive(Clone)] struct DbRes; 233 | impl FromResources for DbRes {} 234 | 235 | // Pattern 1: Simple list of handlers 236 | let router1: Router = router_builder!(handler_one, handler_two).build(); 237 | 238 | // Pattern 2: Explicit handlers and resources lists 239 | let router2: Router = router_builder!( 240 | handlers: [handler_one, handler_two], 241 | resources: [DbRes{}] 242 | ).build(); 243 | 244 | // Pattern 3: Explicit handlers list only 245 | let router3: Router = router_builder!( 246 | handlers: [handler_one] 247 | ).build(); 248 | ``` 249 | 250 | ## Error Handling 251 | 252 | - **`router::Error`**: Enum representing errors occurring *within* the router logic or during handler invocation setup (parameter parsing, resource fetching). This is the error type within `CallResult::Err(CallError)`. 253 | ```rust 254 | pub enum Error { 255 | // -- Into Params Errors 256 | ParamsParsing(serde_json::Error), 257 | ParamsMissingButRequested, 258 | 259 | // -- Router Error 260 | MethodUnknown, 261 | 262 | // -- Handler Setup/Execution Errors 263 | FromResources(FromResourcesError), 264 | HandlerResultSerialize(serde_json::Error), // Error serializing the handler's Ok(value) 265 | Handler(HandlerError), // Wrapper for an error returned *by* the handler itself (Err(handler_error)) 266 | } 267 | ``` 268 | Implements `Debug`, `Serialize`, `Display`, `std::error::Error`. `From`, `From`. 269 | - **`FromResourcesError`**: Error specifically from failing to get a resource via `FromResources`. 270 | ```rust 271 | pub enum FromResourcesError { 272 | ResourceNotFound(&'static str), // Contains the name of the type not found. 273 | } 274 | ``` 275 | Implements `Debug`, `Serialize`, `Display`, `std::error::Error`. 276 | - **`HandlerError`**: Wrapper type for errors returned *by* the handler function (`Err(handler_error)`). Allows retrieving the original error type. 277 | - `new(val: T) -> HandlerError` 278 | - `get(&self) -> Option<&T>`: Attempt to downcast the contained error back to its original type `T`. 279 | - `remove(&mut self) -> Option`: Downcast and take ownership. 280 | - `type_name(&self) -> &'static str`: Get the type name of the contained error. 281 | - Implements `Debug`, `Serialize` (serializes type name info), `Display`, `std::error::Error`. 282 | - **`IntoHandlerError` Trait**: Converts any error (`T: Any + Send + Sync + 'static`) into a `HandlerError`. Automatically implemented for types meeting the bounds (including `HandlerError`, `String`, `&'static str`, `Value`). Allows handlers to use `?` or return `Result` which automatically converts `MyCustomError` into `HandlerError` when the handler signature specifies `-> HandlerResult`. 283 | - **`#[derive(RpcHandlerError)]`** (requires `rpc-router-macros` feature): Derive macro to implement `std::error::Error` and `IntoHandlerError` (via `From for HandlerError`) for custom error enums returned by handlers. This simplifies returning custom errors. 284 | 285 | ## JSON-RPC Response (`RpcResponse`, `RpcError`) 286 | 287 | These types represent the final JSON-RPC 2.0 response sent back to the client. They are typically created by converting from `CallResult`. 288 | 289 | - **`RpcResponse`**: Enum representing a standard JSON-RPC 2.0 response. Contains either `RpcSuccessResponse` or `RpcErrorResponse`. 290 | ```rust 291 | pub enum RpcResponse { 292 | Success(RpcSuccessResponse), 293 | Error(RpcErrorResponse), 294 | } 295 | ``` 296 | - Implements `Serialize`, `Deserialize`, `Debug`, `Clone`, `PartialEq`. 297 | - Provides `from_success(id, result)` and `from_error(id, error)` constructors. 298 | - Provides `id()`, `is_success()`, `is_error()`, `into_parts()`. 299 | - **`RpcSuccessResponse`**: Struct holding the `id` and `result` (a `Value`) for a successful response. 300 | ```rust 301 | pub struct RpcSuccessResponse { 302 | pub id: RpcId, 303 | pub result: Value, 304 | } 305 | ``` 306 | - **`RpcErrorResponse`**: Struct holding the `id` and `error` (`RpcError`) for an error response. 307 | ```rust 308 | pub struct RpcErrorResponse { 309 | pub id: RpcId, 310 | pub error: RpcError, 311 | } 312 | ``` 313 | - **`RpcError`**: Struct representing the JSON-RPC 2.0 Error Object. 314 | ```rust 315 | pub struct RpcError { 316 | pub code: i64, 317 | pub message: String, 318 | #[serde(skip_serializing_if = "Option::is_none")] 319 | pub data: Option, 320 | } 321 | ``` 322 | - Includes constants for standard JSON-RPC error codes (`CODE_PARSE_ERROR`, `CODE_INVALID_REQUEST`, `CODE_METHOD_NOT_FOUND`, `CODE_INVALID_PARAMS`, `CODE_INTERNAL_ERROR`). 323 | - Includes constructor functions like `from_parse_error(data)`, `from_invalid_request(data)`, etc. 324 | - Implements `Serialize`, `Deserialize`, `Debug`, `Clone`, `PartialEq`. 325 | - **Conversion**: 326 | - `From for RpcResponse`: Creates `RpcResponse::Success`. 327 | - `From for RpcResponse`: Creates `RpcResponse::Error` by converting `CallError.error` into an `RpcError`. 328 | - `From for RpcResponse`: Handles `Ok(CallSuccess)` and `Err(CallError)`. 329 | - `From<&router::Error> for RpcError`: Converts internal `router::Error` variants into standard `RpcError` structures (mapping codes and messages, placing original error string representation in `data`). 330 | - `From for RpcError`: Converts `CallError` into `RpcError`. 331 | - `From<&CallError> for RpcError`. 332 | - **Serialization/Deserialization**: `RpcResponse` implements `Serialize` and `Deserialize` according to the JSON-RPC 2.0 specification, including strict validation during deserialization (e.g., checks for `"jsonrpc": "2.0"`, presence of `id`, mutual exclusion of `result` and `error`). 333 | - **`RpcResponseParsingError`**: Enum detailing errors during `RpcResponse` deserialization (e.g., `InvalidJsonRpcVersion`, `BothResultAndError`, `MissingId`, `InvalidErrorObject`). 334 | 335 | ## Utility Macros 336 | 337 | - **`router_builder!(...)`**: Creates a `RouterBuilder` (see Router Configuration). 338 | - **`resources_builder!(...)`**: Creates a `ResourcesBuilder` (see Resources Management). 339 | - **`#[derive(RpcParams)]`**: Implements `IntoParams`. 340 | - **`#[derive(RpcResource)]`**: Implements `FromResources`. 341 | - **`#[derive(RpcHandlerError)]`**: Implements error boilerplate for handler errors (`IntoHandlerError`). 342 | --------------------------------------------------------------------------------