lsps: Implement JSON-RPC V2 client
Adds an async safe JSON-RPC V2 client for a generic transport layer. The transport layer we will use later on are BOLT8 lightning messages. Signed-off-by: Peter Neuroth <pet.v.ne@gmail.com>
This commit is contained in:
committed by
ShahanaFarooqui
parent
ea5635c4c8
commit
203621a629
196
Cargo.lock
generated
196
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
351
plugins/lsps-plugin/src/jsonrpc/client.rs
Normal file
351
plugins/lsps-plugin/src/jsonrpc/client.rs
Normal file
@@ -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<String, Error>;
|
||||
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<T: Transport> {
|
||||
transport: Arc<T>,
|
||||
}
|
||||
|
||||
impl<T: Transport> JsonRpcClient<T> {
|
||||
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<Value>) -> Result<Value> {
|
||||
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<RQ, RS>(&self, request: RQ) -> Result<RS>
|
||||
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<Value>) -> 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<RQ>(&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<RS, RP>(
|
||||
&self,
|
||||
method: &str,
|
||||
payload: &RP,
|
||||
id: String,
|
||||
) -> Result<ResponseObject<RS>>
|
||||
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<RP>(&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<OnceCell<String>>,
|
||||
res: Arc<Option<String>>,
|
||||
err: Arc<Option<String>>,
|
||||
}
|
||||
|
||||
impl TestTransport {
|
||||
// Get the last request as parsed JSON
|
||||
fn last_request_json(&self) -> Option<Value> {
|
||||
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<String, Error> {
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<T: core::fmt::Display>(v: T) -> Self {
|
||||
return Self::Other(v.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TransportError {
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// 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<T>) -> Result<T, RpcError> {
|
||||
fn from_response(resp: ResponseObject<T>) -> Result<T>
|
||||
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<T> ResponseObject<T>
|
||||
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<T, RpcError> {
|
||||
pub fn into_inner(self) -> Result<T> {
|
||||
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]
|
||||
|
||||
0
plugins/lsps-plugin/src/jsonrpc/server.rs
Normal file
0
plugins/lsps-plugin/src/jsonrpc/server.rs
Normal file
Reference in New Issue
Block a user