lsps: Add JSON-RPC V2 server
This commit is contained in:
committed by
ShahanaFarooqui
parent
203621a629
commit
68ca86ca4f
67
Cargo.lock
generated
67
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user