Expand description
JSON-RPC 2.0 types and an async server.
This crate provides pure data types for JSON-RPC 2.0 messages, plus a
Server for method registration and dispatch. It is transport-agnostic
and performs no I/O, so the caller drives the wire.
§Features
- Zero-copy deferred parsing via
serde_json::value::RawValue - Generic
Server<C>with typed async handler registration and automatic serde (de)serialization - Spec-compliant batch request/response handling, including invalid batch entries
- Notification support
- Owned context:
Cis cloned once per handler invocation (wrap it in anArcfor cheap clones)
§Defining typed parameters and results
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct AddParams {
a: i64,
b: i64,
}
#[derive(Serialize)]
struct AddResult {
sum: i64,
}§Creating a server
use json_rpc::{Error, Server, ServerConfig};
#[derive(Clone)]
struct AppState {
prefix: String,
}
let mut server: Server<AppState> = Server::new(ServerConfig::default());
server.register("add", |ctx: AppState, params: AddParams| async move {
Ok::<_, Error>(AddResult {
sum: params.a + params.b,
})
});§Handling requests
A single request:
let json = r#"{"jsonrpc":"2.0","method":"add","params":[3,4],"id":1}"#;
let request: Request = serde_json::from_str(json).unwrap();
let response = server.handle((), RequestMessage::Single(request)).await;
if let Some(response) = response {
println!("{}", serde_json::to_string(&response).unwrap());
// => {"jsonrpc":"2.0","result":7,"id":1}
}A notification has no id, so Server::handle returns None and nothing
should be sent back:
let json = r#"{"jsonrpc":"2.0","method":"log","params":"hello"}"#;
let request: Request = serde_json::from_str(json).unwrap();
let response = server.handle((), RequestMessage::Single(request)).await;
assert!(response.is_none());A batch:
let json = r#"[
{"jsonrpc":"2.0","method":"add","params":[1,2],"id":"1"},
{"jsonrpc":"2.0","method":"add","params":[3,4],"id":"2"}
]"#;
let message: RequestMessage = serde_json::from_str(json).unwrap();
let response = server.handle((), message).await;
if let Some(response) = response {
println!("{}", serde_json::to_string(&response).unwrap());
}§Custom errors
A handler may return any error type that converts into Error:
#[derive(Debug)]
enum AppError {
DivisionByZero,
Db(String),
}
impl From<AppError> for Error {
fn from(e: AppError) -> Self {
match e {
AppError::DivisionByZero => Error::new(-32000, "division by zero"),
AppError::Db(message) => Error::new(-32001, format!("internal: {message}")),
}
}
}
server.register("div", |_: (), (a, b): (i64, i64)| async move {
if b == 0 {
Err(AppError::DivisionByZero)
} else {
Ok(a / b)
}
});§Serving over a transport
The server is transport-agnostic. For example, over a newline-delimited TCP stream:
use json_rpc::{Error, Id, RequestMessage, Response, Server, ServerConfig};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut server = Server::<()>::new(ServerConfig::default());
server.register("ping", |_: (), ()| async move { Ok::<_, Error>("pong".to_string()) });
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (stream, _) = listener.accept().await?;
let (reader, mut writer) = stream.into_split();
let mut lines = BufReader::new(reader).lines();
while let Ok(Some(line)) = lines.next_line().await {
let message: RequestMessage = match serde_json::from_str(&line) {
Ok(message) => message,
Err(_) => {
let error = Response::error(Id::Null, Error::parse_error());
writer.write_all(serde_json::to_string(&error).unwrap().as_bytes()).await?;
writer.write_all(b"\n").await?;
continue;
}
};
if let Some(response) = server.handle((), message).await {
writer.write_all(serde_json::to_string(&response).unwrap().as_bytes()).await?;
writer.write_all(b"\n").await?;
}
}
}
}Over HTTP, deserialize a RequestMessage from the request body, call
Server::handle, and serialize the returned ResponseMessage (or reply
204 No Content when it is None):
ⓘ
use std::sync::Arc;
use axum::{
extract::State,
extract::rejection::JsonRejection,
http::StatusCode,
response::IntoResponse,
routing::post,
Json, Router,
};
use json_rpc::{Error, Id, RequestMessage, Response, Server, ServerConfig};
async fn rpc_handler(
State(server): State<Arc<Server<AppState>>>,
State(state): State<Arc<AppState>>,
payload: Result<Json<RequestMessage>, JsonRejection>,
) -> impl IntoResponse {
let message = match payload {
Ok(Json(message)) => message,
Err(_) => {
let error = Response::error(Id::Null, Error::parse_error());
return (StatusCode::OK, Json(error)).into_response();
}
};
match server.handle((*state).clone(), message).await {
Some(response) => (StatusCode::OK, Json(response)).into_response(),
None => StatusCode::NO_CONTENT.into_response(),
}
}Re-exports§
pub use error_code::ErrorCode;pub use id::Id;pub use request::Request;pub use request::RequestId;pub use request::RequestMessage;pub use response::Error;pub use response::Response;pub use server::ResponseMessage;pub use server::Server;pub use server::ServerConfig;