lsps: Add JSON-RPC V2 server

This commit is contained in:
Peter Neuroth
2025-03-29 02:57:13 +01:00
committed by ShahanaFarooqui
parent 203621a629
commit 68ca86ca4f
3 changed files with 331 additions and 42 deletions

67
Cargo.lock generated
View File

@@ -423,7 +423,7 @@ dependencies = [
"dashmap",
"hex",
"log",
"rand 0.9.0",
"rand 0.9.1",
"serde",
"serde_json",
"thiserror 2.0.11",
@@ -812,14 +812,14 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"windows-targets",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
@@ -1553,7 +1553,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy 0.7.35",
"zerocopy",
]
[[package]]
@@ -1637,6 +1637,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.8.5"
@@ -1650,13 +1656,12 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy 0.8.23",
]
[[package]]
@@ -1694,7 +1699,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.1",
"getrandom 0.3.2",
]
[[package]]
@@ -1713,9 +1718,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.10"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
dependencies = [
"bitflags 2.8.0",
]
@@ -2053,9 +2058,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
dependencies = [
"libc",
]
@@ -2309,9 +2314,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.44.1"
version = "1.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
dependencies = [
"backtrace",
"bytes",
@@ -2749,9 +2754,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
@@ -2871,9 +2876,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.8.0",
]
@@ -2948,16 +2953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
dependencies = [
"zerocopy-derive 0.8.23",
"zerocopy-derive",
]
[[package]]
@@ -2971,17 +2967,6 @@ dependencies = [
"syn",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.5"

View File

@@ -2,6 +2,7 @@ pub mod client;
use log::debug;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{self, Value};
pub mod server;
use std::fmt;
use thiserror::Error;
@@ -171,7 +172,8 @@ where
/// **REQUIRED**. The identifier of the original request this is a response.
id: String,
/// **REQUIRED on success**. The data if there is a request and non-errored.
/// MUST be `null` if there was an error.
/// MUST NOT exist if there was an error triggered during invocation.
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<T>,
/// **REQUIRED on error** An error type if there was a failure.
error: Option<RpcError>,

View File

@@ -0,0 +1,302 @@
use crate::jsonrpc::{Result, RpcError};
use async_trait::async_trait;
use log::{debug, trace};
use std::{collections::HashMap, sync::Arc};
/// Responsible for writing JSON-RPC responses back to clients.
///
/// This trait abstracts the mechanism for sending responses back to the client,
/// allowing handlers to remain transport-agnostic. Implementations of this
/// trait handle the actual transmission of response data over the underlying
/// transport.
#[async_trait]
pub trait JsonRpcResponseWriter: Send + 'static {
/// Writes the provided payload as a response.
async fn write(&mut self, payload: &[u8]) -> Result<()>;
}
/// Processes JSON-RPC requests and produces responses.
///
/// This trait defines the interface for handling specific JSON-RPC methods.
/// Each method supported by the server should have a corresponding handler
/// that implements this trait.
#[async_trait]
pub trait RequestHandler: Send + Sync + 'static {
/// Handles a JSON-RPC request.
async fn handle(&self, payload: &[u8]) -> core::result::Result<Vec<u8>, RpcError>;
}
/// Builder for creating JSON-RPC servers.
pub struct JsonRpcServerBuilder {
handlers: HashMap<String, Arc<dyn RequestHandler>>,
}
impl JsonRpcServerBuilder {
pub fn new() -> Self {
Self {
handlers: HashMap::new(),
}
}
/// Registers a handler for a specific JSON-RPC method.
pub fn with_handler(mut self, method: String, handler: Arc<dyn RequestHandler>) -> Self {
self.handlers.insert(method, handler);
self
}
/// Builds a JSON-RPC server with the configured handlers.
pub fn build(self) -> JsonRpcServer {
JsonRpcServer {
handlers: Arc::new(self.handlers),
}
}
}
/// Server for handling JSON-RPC 2.0 requests.
///
/// Dispatches incoming JSON-RPC requests to the appropriate handlers based on
/// the method name, and manages the response lifecycle.
#[derive(Clone)]
pub struct JsonRpcServer {
handlers: Arc<HashMap<String, Arc<dyn RequestHandler>>>,
}
impl JsonRpcServer {
pub fn builder() -> JsonRpcServerBuilder {
JsonRpcServerBuilder::new()
}
// Processes a JSON-RPC message and writes the response.
///
/// This is the main entry point for handling JSON-RPC requests. It:
/// 1. Parses and validates the incoming request
/// 2. Routes the request to the appropriate handler
/// 3. Writes the response back to the client (if needed)
pub async fn handle_message(
&self,
payload: &[u8],
writer: &mut dyn JsonRpcResponseWriter,
) -> Result<()> {
trace!("Handle request with payload: {:?}", payload);
let value: serde_json::Value = serde_json::from_slice(payload)?;
let id = value.get("id").and_then(|id| id.as_str());
let method = value.get("method").and_then(|method| method.as_str());
let jsonrpc = value.get("jsonrpc").and_then(|jrpc| jrpc.as_str());
trace!(
"Validate request: id={:?}, method={:?}, jsonrpc={:?}",
id,
method,
jsonrpc
);
let method = match (jsonrpc, method) {
(Some(jrpc), Some(method)) if jrpc == "2.0" => method,
(_, _) => {
debug!("Got invalid request {}", value);
let err = RpcError {
code: -32600,
message: "Invalid request".into(),
data: None,
};
return self.maybe_write_error(id, err, writer).await;
}
};
trace!("Get handler for id={:?}, method={:?}", id, method);
if let Some(handler) = self.handlers.get(method) {
trace!(
"Call handler for id={:?}, method={:?}, with payload={:?}",
id,
method,
payload
);
match handler.handle(payload).await {
Ok(res) => return self.maybe_write(id, &res, writer).await,
Err(e) => {
debug!("Handler returned with error: {}", e);
return self.maybe_write_error(id, e, writer).await;
}
};
} else {
debug!("No handler found for method: {}", method);
let err = RpcError {
code: -32601,
message: "Method not found".into(),
data: None,
};
return self.maybe_write_error(id, err, writer).await;
}
}
/// Writes a response if the request has an ID.
///
/// For notifications (requests without an ID), no response is written.
async fn maybe_write(
&self,
id: Option<&str>,
payload: &[u8],
writer: &mut dyn JsonRpcResponseWriter,
) -> Result<()> {
// No need to respond when we don't have an id - it's a notification
if id.is_some() {
return writer.write(payload).await;
}
Ok(())
}
/// Writes an error response if the request has an ID.
///
/// For notifications (requests without an ID), no response is written.
async fn maybe_write_error(
&self,
id: Option<&str>,
err: RpcError,
writer: &mut dyn JsonRpcResponseWriter,
) -> Result<()> {
// No need to respond when we don't have an id - it's a notification
if let Some(id) = id {
let err_res = err.clone().into_response(id.into());
let err_vec = serde_json::to_vec(&err_res).map_err(|e| RpcError::internal_error(e))?;
return writer.write(&err_vec).await;
}
Ok(())
}
}
#[cfg(test)]
mod test_json_rpc_server {
use super::*;
#[derive(Default)]
struct MockWriter {
log_content: String,
}
#[async_trait]
impl JsonRpcResponseWriter for MockWriter {
async fn write(&mut self, payload: &[u8]) -> Result<()> {
println!("Write payload={:?}", &payload);
let byte_str = String::from_utf8(payload.to_vec()).unwrap();
self.log_content = byte_str;
Ok(())
}
}
// Echo handler
pub struct Echo;
#[async_trait]
impl RequestHandler for Echo {
async fn handle(&self, payload: &[u8]) -> core::result::Result<Vec<u8>, RpcError> {
println!("Called handler with payload: {:?}", &payload);
Ok(payload.to_vec())
}
}
#[tokio::test]
async fn test_notification() {
// A notification should not respond to the client so there is no need
// to write payload to the writer;
let server = JsonRpcServer::builder()
.with_handler("echo".to_string(), Arc::new(Echo))
.build();
let mut writer = MockWriter {
log_content: String::default(),
};
let msg = r#"{"jsonrpc":"2.0","method":"echo","params":{"age":99,"name":"Satoshi"}}"#; // No id signals a notification.
let res = server.handle_message(msg.as_bytes(), &mut writer).await;
assert!(res.is_ok());
assert!(writer.log_content.is_empty()); // Was a notification we don't expect a response;
}
#[tokio::test]
async fn missing_method_field() {
// We verify the request data, check that we return an error when we
// don't understand the request.
let server = JsonRpcServer::builder()
.with_handler("echo".to_string(), Arc::new(Echo))
.build();
let mut writer = MockWriter {
log_content: String::default(),
};
let msg = r#"{"jsonrpc":"2.0","params":{"age":99,"name":"Satoshi"},"id":"unique-id-123"}"#;
let res = server.handle_message(msg.as_bytes(), &mut writer).await;
assert!(res.is_ok());
let expected = r#"{"jsonrpc":"2.0","id":"unique-id-123","error":{"code":-32600,"message":"Invalid request"}}"#; // Unknown method say_hello
assert_eq!(writer.log_content, expected);
}
#[tokio::test]
async fn wrong_version() {
// We only accept requests that have jsonrpc version 2.0.
let server = JsonRpcServer::builder()
.with_handler("echo".to_string(), Arc::new(Echo))
.build();
let mut writer = MockWriter {
log_content: String::default(),
};
let msg = r#"{"jsonrpc":"1.0","method":"echo","params":{"age":99,"name":"Satoshi"},"id":"unique-id-123"}"#;
let res = server.handle_message(msg.as_bytes(), &mut writer).await;
assert!(res.is_ok());
let expected = r#"{"jsonrpc":"2.0","id":"unique-id-123","error":{"code":-32600,"message":"Invalid request"}}"#; // Unknown method say_hello
assert_eq!(writer.log_content, expected);
}
#[tokio::test]
async fn propper_request() {
// Check that we call the handler and write back to the writer when
// processing a well-formed request.
let server = JsonRpcServer::builder()
.with_handler("echo".to_string(), Arc::new(Echo))
.build();
let mut writer = MockWriter {
log_content: String::default(),
};
let msg = r#"{"jsonrpc":"2.0","method":"echo","params":{"age":99,"name":"Satoshi"},"id":"unique-id-123"}"#;
let res = server.handle_message(msg.as_bytes(), &mut writer).await;
assert!(res.is_ok());
assert_eq!(writer.log_content, msg.to_string());
}
#[tokio::test]
async fn unknown_method() {
// We don't know the method and need to send back an error to the client.
let server = JsonRpcServer::builder()
.with_handler("echo".to_string(), Arc::new(Echo))
.build();
let mut writer = MockWriter {
log_content: String::default(),
};
let msg = r#"{"jsonrpc":"2.0","method":"say_hello","params":{"age":99,"name":"Satoshi"},"id":"unique-id-123"}"#; // Unknown method say_hello
let res = server.handle_message(msg.as_bytes(), &mut writer).await;
assert!(res.is_ok());
let expected = r#"{"jsonrpc":"2.0","id":"unique-id-123","error":{"code":-32601,"message":"Method not found"}}"#; // Unknown method say_hello
assert_eq!(writer.log_content, expected);
}
#[tokio::test]
async fn test_handler() {
let server = JsonRpcServer::builder()
.with_handler("echo".to_string(), Arc::new(Echo))
.build();
let mut writer = MockWriter {
log_content: String::default(),
};
let msg = r#"{"jsonrpc":"2.0","method":"echo","params":{"age":99,"name":"Satoshi"},"id":"unique-id-123"}"#;
let res = server.handle_message(msg.as_bytes(), &mut writer).await;
assert!(res.is_ok());
assert_eq!(writer.log_content, msg.to_string());
}
}