diff --git a/Cargo.lock b/Cargo.lock index 81a8393c7..0483e0481 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -419,8 +419,15 @@ dependencies = [ name = "cln-lsps" version = "0.1.0" dependencies = [ + "async-trait", + "dashmap", + "hex", + "log", + "rand 0.9.0", "serde", "serde_json", + "thiserror 2.0.11", + "tokio", ] [[package]] @@ -519,6 +526,20 @@ dependencies = [ "typenum", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.7.0" @@ -602,7 +623,7 @@ dependencies = [ "hyper 1.5.2", "hyper-util", "pin-project-lite", - "rand", + "rand 0.8.5", "serde", "serde_json", "smallvec", @@ -786,7 +807,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", ] [[package]] @@ -839,6 +872,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.2" @@ -1231,6 +1270,16 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "lockfree-object-pool" version = "0.1.6" @@ -1317,7 +1366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1411,6 +1460,29 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "pem" version = "3.0.4" @@ -1481,7 +1553,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -1572,8 +1644,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy 0.8.23", ] [[package]] @@ -1583,7 +1666,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -1592,7 +1685,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.1", ] [[package]] @@ -1609,6 +1711,15 @@ dependencies = [ "yasna", ] +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "regex" version = "1.11.1" @@ -1661,7 +1772,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -1813,6 +1924,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sct" version = "0.7.1" @@ -1934,6 +2051,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2075,7 +2201,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.2.15", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2183,15 +2309,17 @@ dependencies = [ [[package]] name = "tokio" -version = "1.43.0" +version = "1.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" dependencies = [ "backtrace", "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -2343,7 +2471,7 @@ dependencies = [ "indexmap 1.9.3", "pin-project", "pin-project-lite", - "rand", + "rand 0.8.5", "slab", "tokio", "tokio-util", @@ -2474,7 +2602,7 @@ dependencies = [ "http 1.2.0", "httparse", "log", - "rand", + "rand 0.8.5", "sha1", "thiserror 1.0.69", "utf-8", @@ -2619,6 +2747,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2732,6 +2869,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -2802,7 +2948,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "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", ] [[package]] @@ -2816,6 +2971,17 @@ 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/Cargo.toml b/plugins/lsps-plugin/Cargo.toml index f265571ba..8f7fd9248 100644 --- a/plugins/lsps-plugin/Cargo.toml +++ b/plugins/lsps-plugin/Cargo.toml @@ -4,5 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] +async-trait = "0.1" +dashmap = "6.1" +hex = "0.4" +log = "0.4" +rand = "0.9" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +thiserror = "2.0" +tokio = { version = "1.44", features = ["full"] } diff --git a/plugins/lsps-plugin/src/jsonrpc/client.rs b/plugins/lsps-plugin/src/jsonrpc/client.rs new file mode 100644 index 000000000..5e0fe167d --- /dev/null +++ b/plugins/lsps-plugin/src/jsonrpc/client.rs @@ -0,0 +1,351 @@ +use async_trait::async_trait; +use core::fmt::Debug; +use log::{debug, error}; +use rand::rngs::OsRng; +use rand::TryRngCore; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::Value; +use std::sync::Arc; + +use crate::jsonrpc::{ + Error, JsonRpcRequest, JsonRpcResponse, RequestObject, ResponseObject, Result, +}; + +/// Defines the interface for transporting JSON-RPC messages. +/// +/// Implementors of this trait are responsible for actually sending the JSON-RPC +/// request over some transport mechanism (RPC, Bolt8, etc.) +#[async_trait] +pub trait Transport { + async fn send(&self, request: String) -> core::result::Result; + async fn notify(&self, request: String) -> core::result::Result<(), Error>; +} + +/// A typed JSON-RPC client that works with any transport implementation. +/// +/// This client handles the JSON-RPC protocol details including message +/// formatting, request ID generation, and response parsing. +#[derive(Clone)] +pub struct JsonRpcClient { + transport: Arc, +} + +impl JsonRpcClient { + pub fn new(transport: T) -> Self { + Self { + transport: Arc::new(transport), + } + } + + /// Makes a JSON-RPC method call with raw JSON parameters and returns a raw + /// JSON result. + pub async fn call_raw(&self, method: &str, params: Option) -> Result { + let id = generate_random_id(); + + debug!("Preparing request: method={}, id={}", method, id); + let request = RequestObject { + jsonrpc: "2.0".into(), + method: method.into(), + params, + id: Some(id.clone().into()), + }; + let res_obj = self.send_request(method, &request, id).await?; + Value::from_response(res_obj) + } + + /// Makes a typed JSON-RPC method call with a request object and returns a + /// typed response. + /// + /// This method provides type safety by using request and response types + /// that implement the necessary traits. + pub async fn call_typed(&self, request: RQ) -> Result + where + RQ: JsonRpcRequest + Serialize + Send + Sync, + RS: DeserializeOwned + Serialize + Debug + Send + Sync, + { + let method = RQ::METHOD; + let id = generate_random_id(); + + debug!("Preparing request: method={}, id={}", method, id); + let request = request.into_request(Some(id.clone().into())); + let res_obj = self.send_request(method, &request, id).await?; + RS::from_response(res_obj) + } + + /// Sends a notification with raw JSON parameters (no response expected). + pub async fn notify_raw(&self, method: &str, params: Option) -> Result<()> { + debug!("Preparing notification: method={}", method); + let request = RequestObject { + jsonrpc: "2.0".into(), + method: method.into(), + params, + id: None, + }; + Ok(self.send_notification(method, &request).await?) + } + + /// Sends a typed notification (no response expected). + pub async fn notify_typed(&self, request: RQ) -> Result<()> + where + RQ: JsonRpcRequest + Serialize + Send + Sync, + { + let method = RQ::METHOD; + + debug!("Preparing notification: method={}", method); + let request = request.into_request(None); + Ok(self.send_notification(method, &request).await?) + } + + async fn send_request( + &self, + method: &str, + payload: &RP, + id: String, + ) -> Result> + where + RP: Serialize + Send + Sync, + RS: DeserializeOwned + Serialize + Debug + Send + Sync, + { + let request_json = serde_json::to_string(&payload)?; + debug!( + "Sending request: method={}, id={}, request={:?}", + method, id, &request_json + ); + let start = tokio::time::Instant::now(); + let res_str = self.transport.send(request_json).await?; + let elapsed = start.elapsed(); + debug!( + "Received response: method={}, id={}, response={}, elapsed={}ms", + method, + id, + &res_str, + elapsed.as_millis() + ); + Ok(serde_json::from_str(&res_str)?) + } + + async fn send_notification(&self, method: &str, payload: &RP) -> Result<()> + where + RP: Serialize + Send + Sync, + { + let request_json = serde_json::to_string(&payload)?; + debug!("Sending notification: method={}", method); + let start = tokio::time::Instant::now(); + self.transport.notify(request_json).await?; + let elapsed = start.elapsed(); + debug!( + "Sent notification: method={}, elapsed={}ms", + method, + elapsed.as_millis() + ); + Ok(()) + } +} + +/// Generates a random ID for JSON-RPC requests. +/// +/// Uses a secure random number generator to create a hex-encoded ID. Falls back +/// to a timestamp-based ID if random generation fails. +fn generate_random_id() -> String { + let mut bytes = [0u8; 10]; + match OsRng.try_fill_bytes(&mut bytes) { + Ok(_) => hex::encode(bytes), + Err(e) => { + // Fallback to a timestamp-based ID if random generation fails + error!( + "Failed to generate random ID: {}, falling back to timestamp", + e + ); + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + format!("fallback-{}", timestamp) + } + } +} + +#[cfg(test)] + +mod test_json_rpc { + use serde::Deserialize; + use tokio::sync::OnceCell; + + use super::*; + use crate::jsonrpc::{self, RpcError}; + + #[derive(Clone)] + struct TestTransport { + req: Arc>, + res: Arc>, + err: Arc>, + } + + impl TestTransport { + // Get the last request as parsed JSON + fn last_request_json(&self) -> Option { + self.req + .get() + .and_then(|req_str| serde_json::from_str(req_str).ok()) + } + } + + #[async_trait] + impl Transport for TestTransport { + async fn send(&self, req: String) -> core::result::Result { + // Store the request + let _ = self.req.set(req); + + // Check for error first + if let Some(err) = &*self.err { + return Err(Error::Transport(jsonrpc::TransportError::Other(err.into()))); + } + + // Then check for response + if let Some(res) = &*self.res { + return Ok(res.clone()); + } + + panic!("TestTransport: neither result nor error is set."); + } + + async fn notify(&self, req: String) -> core::result::Result<(), Error> { + // Store the request + let _ = self.req.set(req); + + // Check for error + if let Some(err) = &*self.err { + return Err(Error::Transport(jsonrpc::TransportError::Other(err.into()))); + } + + Ok(()) + } + } + + #[derive(Default, Clone, Serialize, Deserialize, Debug)] + struct DummyCall { + foo: String, + bar: i32, + } + + impl JsonRpcRequest for DummyCall { + const METHOD: &'static str = "dummy_call"; + } + + #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] + struct DummyResponse { + foo: String, + bar: i32, + } + + #[tokio::test] + async fn test_typed_call_w_response() { + let req = DummyCall { + foo: String::from("hello world!"), + bar: 13, + }; + + let expected_res = DummyResponse { + foo: String::from("hello client!"), + bar: 10, + }; + + let res_obj = expected_res + .clone() + .into_response(String::from("unique-id-123")); + let res_str = serde_json::to_string(&res_obj).unwrap(); + + let transport = TestTransport { + req: Arc::new(OnceCell::const_new()), + res: Arc::new(Some(res_str)), + err: Arc::new(None), + }; + + let client_1 = JsonRpcClient::new(transport.clone()); + let res = client_1 + .call_typed::<_, DummyResponse>(req.clone()) + .await + .expect("Should have an OK result"); + assert_eq!(res, expected_res); + let transport_req = transport + .last_request_json() + .expect("Transport should have gotten a request"); + assert_eq!( + transport_req + .get("jsonrpc") + .and_then(|v| v.as_str()) + .unwrap(), + "2.0" + ); + assert_eq!( + transport_req + .get("params") + .and_then(|v| v.as_object()) + .unwrap(), + serde_json::to_value(&req).unwrap().as_object().unwrap() + ); + } + + #[tokio::test] + async fn test_typed_call_w_rpc_error() { + let req = DummyCall { + foo: "hello world!".into(), + bar: 13, + }; + + let err_res = RpcError::custom_error_with_data( + -32099, + "got a custom error", + serde_json::json!({"got": "some"}), + ); + + let res_obj = err_res.clone().into_response("unique-id-123".into()); + let res_str = serde_json::to_string(&res_obj).unwrap(); + + let transport = TestTransport { + req: Arc::new(OnceCell::const_new()), + res: Arc::new(Some(res_str)), + err: Arc::new(None), + }; + + let client_1 = JsonRpcClient::new(transport); + let res = client_1 + .call_typed::<_, DummyResponse>(req) + .await + .expect_err("Expected error response"); + assert!(match res { + Error::Rpc(rpc_error) => { + assert_eq!(rpc_error, err_res); + true + } + _ => false, + }); + } + + #[tokio::test] + async fn test_typed_call_w_transport_error() { + let req = DummyCall { + foo: "hello world!".into(), + bar: 13, + }; + + let transport = TestTransport { + req: Arc::new(OnceCell::const_new()), + res: Arc::new(None), + err: Arc::new(Some(String::from("transport error"))), + }; + + let client_1 = JsonRpcClient::new(transport); + let res = client_1 + .call_typed::<_, DummyResponse>(req) + .await + .expect_err("Expected error response"); + assert!(match res { + Error::Transport(err) => { + assert_eq!(err.to_string(), "Other error: transport error"); + true + } + _ => false, + }); + } +} diff --git a/plugins/lsps-plugin/src/jsonrpc/mod.rs b/plugins/lsps-plugin/src/jsonrpc/mod.rs index 354f9b34d..7f8af3e87 100644 --- a/plugins/lsps-plugin/src/jsonrpc/mod.rs +++ b/plugins/lsps-plugin/src/jsonrpc/mod.rs @@ -1,6 +1,9 @@ +pub mod client; +use log::debug; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::{self, Value}; use std::fmt; +use thiserror::Error; // Constants for JSON-RPC error codes const PARSE_ERROR: i64 = -32700; @@ -9,6 +12,32 @@ const METHOD_NOT_FOUND: i64 = -32601; const INVALID_PARAMS: i64 = -32602; const INTERNAL_ERROR: i64 = -32603; +#[derive(Error, Debug)] +pub enum Error { + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + #[error("RPC error: {0}")] + Rpc(#[from] RpcError), + #[error("Transport error: {0}")] + Transport(#[from] TransportError), + #[error("Other error: {0}")] + Other(String), +} + +impl Error { + pub fn other(v: T) -> Self { + return Self::Other(v.to_string()); + } +} + +#[derive(Error, Debug)] +pub enum TransportError { + #[error("Other error: {0}")] + Other(String), +} + +pub type Result = std::result::Result; + /// Trait to convert a struct into a JSON-RPC RequestObject. pub trait JsonRpcRequest: Serialize { const METHOD: &'static str; @@ -42,15 +71,22 @@ where } } - fn from_response(resp: ResponseObject) -> Result { + fn from_response(resp: ResponseObject) -> Result + where + T: core::fmt::Debug, + { match (resp.result, resp.error) { (Some(result), None) => Ok(result), - (None, Some(error)) => Err(error), - _ => Err(RpcError { - code: -32603, - message: "Invalid response format".into(), - data: None, - }), + (None, Some(error)) => Err(Error::Rpc(error)), + _ => { + debug!( + "Invalid JSON-RPC response - missing both result and error fields, or both set: id={}", + resp.id + ); + Err(Error::Rpc(RpcError::internal_error( + "not a valid json respone", + ))) + } } } } @@ -143,11 +179,11 @@ where impl ResponseObject where - T: DeserializeOwned + Serialize, + T: DeserializeOwned + Serialize + core::fmt::Debug, { /// Returns a potential data (result) if the code execution passed else it /// returns with RPC error, data (error details) if there was - pub fn into_inner(self) -> Result { + pub fn into_inner(self) -> Result { T::from_response(self) } } @@ -422,12 +458,17 @@ mod test_message_serialization { let response = response_object.into_inner(); let err = response.unwrap_err(); - assert_eq!(err.code, -32099); - assert_eq!(err.message, "something bad happened"); - assert_eq!( - err.data, - serde_json::from_str("{\"f1\":\"v1\",\"f2\":2}").unwrap() - ); + match err { + Error::Rpc(err) => { + assert_eq!(err.code, -32099); + assert_eq!(err.message, "something bad happened"); + assert_eq!( + err.data, + serde_json::from_str("{\"f1\":\"v1\",\"f2\":2}").unwrap() + ); + } + _ => assert!(false), + } } #[test] diff --git a/plugins/lsps-plugin/src/jsonrpc/server.rs b/plugins/lsps-plugin/src/jsonrpc/server.rs new file mode 100644 index 000000000..e69de29bb