diff --git a/Cargo.lock b/Cargo.lock index 0483e0481..39f95d76d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/plugins/lsps-plugin/src/jsonrpc/mod.rs b/plugins/lsps-plugin/src/jsonrpc/mod.rs index 7f8af3e87..ea2ac0745 100644 --- a/plugins/lsps-plugin/src/jsonrpc/mod.rs +++ b/plugins/lsps-plugin/src/jsonrpc/mod.rs @@ -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, /// **REQUIRED on error** An error type if there was a failure. error: Option, diff --git a/plugins/lsps-plugin/src/jsonrpc/server.rs b/plugins/lsps-plugin/src/jsonrpc/server.rs index e69de29bb..f9e3334b0 100644 --- a/plugins/lsps-plugin/src/jsonrpc/server.rs +++ b/plugins/lsps-plugin/src/jsonrpc/server.rs @@ -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, RpcError>; +} + +/// Builder for creating JSON-RPC servers. +pub struct JsonRpcServerBuilder { + handlers: HashMap>, +} + +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) -> 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>>, +} + +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, 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()); + } +}