9 releases
Uses new Rust 2024
new 0.2.0-alpha.1 | Apr 28, 2025 |
---|---|
0.1.3 | Mar 14, 2024 |
0.0.3 |
|
#172 in Network programming
981 downloads per month
Used in 3 crates
110KB
2K
SLoC
rpc-router - JSON-RPC Routing Library
WARNING: The main branch is now a work in progress for the upcoming v0.2.0
(see the v0.1.1 tag for the 0.1.x version).
v0.2.0-alpha.x Will be released but will have API changes between them. The future -rc.x
will be more stable.
Upcoming API changes for v0.2.0
RpcId
- Now uses a concrete type for RpcId.RpcRequest
- The oldRequest
is now renamedRpcRequest
. The design is that raw JSON-RPC constructs are prefixed withRpc
.RpcNotification
- New type (LikeRpcRequest
but with no.id
as per the spec).RpcResponse
- New type.
Getting Started
rpc-router
is a JSON-RPC 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)
The goal of this library is to enable application functions with different argument types and signatures as follows:
pub async fn create_task(mm: ModelManager, aim: AiManager, params: TaskForCreate) -> Result<i64, MyError> {
// ...
}
pub async fn get_task(mm: ModelManager, params: ParamsIded) -> Result<Task, MyError> {
// ...
}
To be callable from a JSON-RPC request as follows:
// JSON-RPC request coming from Axum route payload, Tauri command params, ...
let rpc_request = json!(
{ jsonrpc: "2.0", id: 1, // required by JSON-RPC
method: "create_task", // method name (matches function name)
params: {title: "First Task"} // optional params (last function argument)
}).try_into()?;
// Async execute the RPC request
let call_response = rpc_router.call(rpc_request).await?;
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:
// Build the Router with the handlers and common resources
let rpc_router = router_builder!(
handlers: [get_task, create_task], // will be turned into routes
resources: [ModelManager {}, AiManager {}] // common resources for all calls
)
.build();
// Can do the same with `Router::builder().append(...)/append_resource(...)`
// Create and parse rpc request example.
let rpc_request: rpc_router::Request = json!({
"jsonrpc": "2.0",
"id": "some-client-req-id", // JSON-RPC request id. Can be null, num, string, but must be present.
"method": "create_task",
"params": { "title": "First task" } // optional.
}).try_into()?;
// Async execute the RPC request.
let call_response = rpc_router.call(rpc_resources, rpc_request).await?;
// Or `call_with_resources` for additional per-call resources that override router common resources.
// e.g., rpc_router.call_with_resources(rpc_request, additional_resources)
// Display the response.
let CallSuccess { id, method, value } = call_response;
println!(
r#"RPC call response:
id: {id:?},
method: {method},
value: {value:?},
"#
);
See examples/c00-readme.rs for the complete working code.
For the above to work, here are the requirements for the various types:
ModelManager
andAiManager
are rpc-router Resources. These types just need to implementrpc_router::FromResources
(the trait has a default implementation, andRpcResource
derive macros can generate this one-liner implementation).
// Make it a Resource with RpcResource derive macro
#[derive(Clone, RpcResource)]
pub struct ModelManager {}
// Make it a Resource by implementing FromResources
#[derive(Clone)]
pub struct AiManager {}
impl FromResources for AiManager {}
TaskForCreate
andParamsIded
are used as JSON-RPC Params and must implement therpc_router::IntoParams
trait, which has a default implementation, and can also be implemented byRpcParams
derive macros.
// Make it a Params with RpcParams derive macro
#[derive(Serialize, Deserialize, RpcParams)]
pub struct TaskForCreate {
title: String,
done: Option<bool>,
}
// Make it a Params by implementing IntoParams
#[derive(Deserialize)]
pub struct ParamsIded {
pub id: i64,
}
impl IntoParams for ParamsIded {}
Task
, as a returned value, just needs to implementserde::Serialize
#[derive(Serialize)]
pub struct Task {
id: i64,
title: String,
done: bool,
}
MyError
must implementIntoHandlerError
, which also has a default implementation, and can also be implemented byRpcHandlerError
derive macros.
#[derive(Debug, thiserror::Error, RpcHandlerError)]
pub enum MyError {
// TBC
#[error("TitleCannotBeEmpty")]
TitleCannotBeEmpty,
}
By the Rust type model, these application errors are set in the HandlerError
and need to be retrieved by handler_error.get::<MyError>()
. See examples/c05-error-handling.rs.
Full code: examples/c00-readme.rs
IMPORTANT
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.Once
0.2.0
is released, I will adhere more strictly to semantic versioning.
Concepts
This library has the following main constructs:
-
Router
- Router is the construct that holds all of the Handler Functions and can be invoked withrouter.call(resources, rpc_request)
. Here are the two main ways to build aRouter
object:- RouterBuilder - via
RouterBuilder::default()
orRouter::builder()
, then call.append(name, function)
or.append_dyn(name, function.into_dyn())
to avoid type monomorphization at the "append" stage. - router_builder! - via the macro
router_builder!(function1, function2, ...)
. This will create, initialize, and return aRouterBuilder
object. - In both cases, call
.build()
to construct the immutable, shareable (via inner Arc)Router
object.
- RouterBuilder - via
-
Resources
- Resources is the type map construct that holds the resources that an RPC handler function might request.- 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. - It's built via
ResourcesBuilder::default().append(my_object)...build()
. - Or via the macro
resources_builder![my_object1, my_object2].build()
. - The
Resources
hold the type map in anArc<>
and are completely immutable and can be cloned effectively. ResourcesBuilder
is not wrapped in anArc<>
, 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.- 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).
- It's similar to Axum State/RequestExtractor or the Tauri State model. In the case of
-
Request
- Is the object that has the JSON-RPC Requestid
,method
, andparams
.- To make a struct a
params
, it has to implement therpc_router::IntoParams
trait, which has a default implementation. - So, implement
impl rpc_router::IntoParams for ... {}
or#[derive(RpcParams)]
. rpc_router::Request::from_value(serde_json::Value) -> Result<Request, RequestParsingError>
will return aRequestParsingError
if the Value does not haveid: Value
,method: String
or if the Value does not contain"jsonrpc": "2.0"
as per the JSON-RPC spec.let request: rpc_router::Request = value.try_into()?
uses the samefrom_value
validation steps.- Doing
serde_json::from_value::<rpc_router::Request>(value)
will not change thejsonrpc
.
- To make a struct a
-
Handler
- RPC handler functions can be any async application function that takes up to 8 resource arguments, plus an optional Params argument.- For example,
async fn create_task(_mm: ModelManager, aim: AiManager, params: TaskForCreate) -> MyResult<i64>
- For example,
-
HandlerError
- RPC handler functions can return their ownResult
as long as the error type implementsIntoHandlerError
, which can be easily implemented asrpc_router::HandlerResult
which includes animpl IntoHandlerError for MyError {}
, or with theRpcHandlerError
derive macro.- To allow handler functions to return their application error,
HandlerError
is essentially a type holder that allows the extraction of the application error withhandler_error.get<MyError>()
. - This requires the application code to know which error type to extract but provides flexibility to return any Error type.
- 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.
- To allow handler functions to return their application error,
-
CallResult
-router.call(...)
will return aCallResult
, which is aResult<CallSuccess, CallError>
where both include the JSON-RPCid
andmethod
name context for future processing.CallError
contains.error: rpc_router::Error
, which includesrpc_router::Error::Handler(HandlerError)
in the event of a handler error.CallSuccess
contains.value: serde_json::Value
, which is the serialized value returned by a successful handler call.
Derive Macros
rpc-router
has some convenient derive proc macros that generate the implementation of various traits.
This is just a stylistic convenience, as the traits themselves have default implementations and are typically one-liner implementations.
Note: These derive proc macros are prefixed with
Rpc
since macros often have generic names, so the prefix adds clarity. Otherrpc-router
types are without the prefix to follow Rust customs.
#[derive(rpc_router::RpcParams)]
Implements rpc_router::IntoParams
for the type.
Works on simple types.
#[derive(serde::Deserialize, rpc_router::RpcParams)]
pub struct ParamsIded {
id: i64
}
// Will generate:
// impl rpc_router::IntoParams for ParamsIded {}
Works with generic types (all will be bound to DeserializeOwned + Send
):
#[derive(rpc_router::RpcParams)]
pub struct ParamsForUpdate<D> {
id: i64,
D
}
// Will generate
// impl<D> IntoParams for ParamsForUpdate<D> where D: DeserializeOwned + Send {}
#[derive(rpc_router::RpcResource)]
Implements the rpc_router::FromResource
trait.
#[derive(Clone, rpc_router::RpcResource)]
pub struct ModelManager;
// Will generate:
// impl FromResources for ModelManager {}
The FromResources
trait has a default implementation to get the T
type (here ModelManager
) from the rpc_router::Resources
type map.
#[derive(rpc_router::RpcHandlerError)]
Implements the rpc_router::IntoHandlerError
trait.
#[derive(Debug, Serialize, RpcHandlerError)]
pub enum MyError {
InvalidName,
// ...
}
// Will generate:
// impl IntoHandlerError for MyError {}
Related Links
- GitHub Repo
- crates.io
- Rust10x rust-web-app (web-app code blueprint using rpc-router with Axum)
Dependencies
~2.8–4MB
~78K SLoC