rpc-router - json-rpc routing library
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 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
pub async
To be callable from a JSON-RPC request as follows:
// json-rpc request comming from Axum route payload, Tauri command params, ...
let rpc_request = json!.try_into?;
// Async Execute the RPC Request
let call_response = rpc_router.call.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!
.build;
// Can do the same with `Router::builder().append(...)/append_resource(...)`
// Create and parse rpc request example.
let rpc_request: Request = json!.try_into?;
// Async Execute the RPC Request.
let call_response = rpc_router.call.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 CallResponse = call_response;
println!;
See examples/c00-readme.rs for the complete functioning code.
For the above to work, here are the requirements for the various types:
ModelManagerandAiManagerare rpc-router Resources. These types just need to implementrpc_router::FromResources(the trait has a default implementation, andRpcResourcederive macros can generate this one-liner implementation).
// Make it a Resource with RpcResource derive macro
// Make it a Resource by implementing FromResources
TaskForCreateandParamIdedare use as JSON-RPC Params and must implement therpc_router::IntoParamstrait, which has a default implementation, and can also be implemented byRpcParamsderive macros.
// Make it a Params with RpcParams derive macro
// Make it a Params by implementing IntoParams
Task, as a returned value just needs implementsserde::Serialize
MyErrormust implement theIntoHandlerError, which also has a default implementation, and can also be implemented byRpcHandlerErrorderive macros.
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.xreleases, 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.0is released, I will adhere more strictly to the semantic versioning methodology.
Note:
v0.1.1changes from0.1.0
router.call(resources, request)was renamed torouter.call_with_resources(request, resources).- Now, the Router can have its own resources, enabling simpler and more efficient sharing of common resources across calls, while still allowing custom resources to be overlaid at the call level.
router.call(request)uses just the default caller resources.See CHANGELOG for more information.
Rust10x rust-web-app has been updated.
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 aRouterobject:- RouterBuilder - via
RouterBuilder::default()orRouter::build(), 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 aRouterBuilderobject. - In both cases, call
.build()to construct the immutable, shareable (via inner Arc)Routerobject.
- RouterBuilder - via
-
Resources- Resources is the type map contstruct that hold the resources that a rpc handler function might request.- It's similar to Axum State/RequestExtractor, or Tauri State model. In the case of
rpc-routerthere in one "domain space" for those states, that are called resources. - It's built via
ResourcesBuilder::default().append(my_object)...build() - Or viat the macro
resources_builder![my_object1, my_object2].build() - The
Resourceshold the "type map" in aArc<>and is completely immutable and can be cloned effectively. ResourcesBuilderis not wrapped inArc<>, 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 value/object inserted in the Resources must implement
Clone + Send + Sync + 'static(in this context the'staticjust means that the type cannot have any reference other than static one )
- It's similar to Axum State/RequestExtractor, or Tauri State model. In the case of
-
Request- Is the object that have the json-rpc Requestid,method, andparams.- To make a struct a
paramsit has to implement therpc_router::IntoParamstrait, which has the 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 andRequestParsingErrorif the Value does not haveid: Value,method: Stringor if the Value does not contain"jsonrpc": "2.0"as per the json-rpc space.let request: rpc_router::Request = value.try_into()?use the samefrom_valuevalidation steps.- Doing
serde_json::from_value::<rpc_router::Request>(value)will not chane 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 ownResultas long as the error type implementsIntoHandlerError, which can be easily implemented asrpc_router::HandlerResultwhich includes animpl IntoHandlerError for MyError {}, or with theRpcHandlerErrorderive macro.- To allow handler functions to return their application error,
HandlerErroris essentially a type holder, which then 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<CallResponse, CallError>where both will include the JSON-RPCidandmethodname context for future processing.CallErrorcontains.error: rpc_router::Error, which includesrpc_router::Error::Handler(HandlerError)in the event of a handler error.CallResponsecontains.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: Those derive proc macros are prefixed with
Rpcas we often tend to just put the proc macro name in the derive, and therefore the prefix adds some clarity. Otherrpc-routertypes, are without the prefix to follow Rust customs.
#[derive(rpc_router::RpcParams)]
Will implement rpc_router::IntoParams for the type.
Works on simple type.
pub strut ParamsIded
// Will generate:
// impl rpc_router::IntoParams for ParamsIded {}
Works with typed with generic (all will be bound to DeserializeOwned + Send)
pub strut
// Will generate
// impl<D> IntoParams for ParamsForCreate<D> where D: DeserializeOwned + Send {}
#[derive(rpc_router::RpcResource)]
Will implement the rpc_router::FromResource trait.
;
// 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)]
Will implment the rpc_router::IntoHandlerError trait.
// 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)