diff --git a/types/src/traits.rs b/types/src/traits.rs index a4b11a4462..79109ac0be 100644 --- a/types/src/traits.rs +++ b/types/src/traits.rs @@ -28,6 +28,8 @@ use crate::v2::params::JsonRpcParams; use crate::{Error, Subscription}; use async_trait::async_trait; use serde::de::DeserializeOwned; +use serde::Serialize; +use serde_json::value::RawValue; /// [JSON-RPC](https://www.jsonrpc.org/specification) client interface that can make requests and notifications. #[async_trait] @@ -81,3 +83,57 @@ pub trait SubscriptionClient: Client { where Notif: DeserializeOwned; } + +/// Marker trait for types that can be serialized as JSON array/sequence. +/// +/// If your type isn't a sequence such as `String`, `usize` or similar. +/// You could insert it in a tuple, slice, array or Vec for it to work. +pub trait ToRpcParams: Serialize { + /// Serialized the type as a JSON array. + fn to_rpc_params(&self) -> Result, serde_json::Error> { + serde_json::to_string(&self).map(|json| RawValue::from_string(json).expect("JSON String; qed")) + } +} + +impl ToRpcParams for &[P] {} + +impl ToRpcParams for Vec

{} + +macro_rules! array_impls { + ($($len:tt)+) => { + $( + impl ToRpcParams for [P; $len] {} + )+ + } +} + +array_impls! { + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 +} + +macro_rules! tuple_impls { + ($($len:expr => ($($n:tt $name:ident)+))+) => { + $( + impl<$($name: Serialize),+> ToRpcParams for ($($name,)+) {} + )+ + } +} + +tuple_impls! { + 1 => (0 T0) + 2 => (0 T0 1 T1) + 3 => (0 T0 1 T1 2 T2) + 4 => (0 T0 1 T1 2 T2 3 T3) + 5 => (0 T0 1 T1 2 T2 3 T3 4 T4) + 6 => (0 T0 1 T1 2 T2 3 T3 4 T4 5 T5) + 7 => (0 T0 1 T1 2 T2 3 T3 4 T4 5 T5 6 T6) + 8 => (0 T0 1 T1 2 T2 3 T3 4 T4 5 T5 6 T6 7 T7) + 9 => (0 T0 1 T1 2 T2 3 T3 4 T4 5 T5 6 T6 7 T7 8 T8) + 10 => (0 T0 1 T1 2 T2 3 T3 4 T4 5 T5 6 T6 7 T7 8 T8 9 T9) + 11 => (0 T0 1 T1 2 T2 3 T3 4 T4 5 T5 6 T6 7 T7 8 T8 9 T9 10 T10) + 12 => (0 T0 1 T1 2 T2 3 T3 4 T4 5 T5 6 T6 7 T7 8 T8 9 T9 10 T10 11 T11) + 13 => (0 T0 1 T1 2 T2 3 T3 4 T4 5 T5 6 T6 7 T7 8 T8 9 T9 10 T10 11 T11 12 T12) + 14 => (0 T0 1 T1 2 T2 3 T3 4 T4 5 T5 6 T6 7 T7 8 T8 9 T9 10 T10 11 T11 12 T12 13 T13) + 15 => (0 T0 1 T1 2 T2 3 T3 4 T4 5 T5 6 T6 7 T7 8 T8 9 T9 10 T10 11 T11 12 T12 13 T13 14 T14) + 16 => (0 T0 1 T1 2 T2 3 T3 4 T4 5 T5 6 T6 7 T7 8 T8 9 T9 10 T10 11 T11 12 T12 13 T13 14 T14 15 T15) +} diff --git a/utils/Cargo.toml b/utils/Cargo.toml index f8c60a8705..3884063d0c 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -9,7 +9,6 @@ license = "MIT" [dependencies] beef = { version = "0.5.1", features = ["impl_serde"] } thiserror = { version = "1", optional = true } -tokio = { version = "1", features = ["macros"], optional = true } futures-channel = { version = "0.3.14", default-features = false, optional = true } futures-util = { version = "0.3.14", default-features = false, optional = true } hyper = { version = "0.14.10", default-features = false, features = ["stream"], optional = true } @@ -35,8 +34,8 @@ server = [ "log", "parking_lot", "rand", - "tokio" ] [dev-dependencies] serde_json = "1.0" +tokio = { version = "1", features = ["macros", "rt"] } \ No newline at end of file diff --git a/utils/src/server/rpc_module.rs b/utils/src/server/rpc_module.rs index 94a132ec17..6275b6f31f 100644 --- a/utils/src/server/rpc_module.rs +++ b/utils/src/server/rpc_module.rs @@ -29,6 +29,7 @@ use beef::Cow; use futures_channel::{mpsc, oneshot}; use futures_util::{future::BoxFuture, FutureExt, StreamExt}; use jsonrpsee_types::error::{CallError, Error, SubscriptionClosedError}; +use jsonrpsee_types::traits::ToRpcParams; use jsonrpsee_types::v2::error::{ JsonRpcErrorCode, JsonRpcErrorObject, CALL_EXECUTION_FAILED_CODE, UNKNOWN_ERROR_CODE, }; @@ -180,6 +181,14 @@ impl Methods { } } + /// Helper to call a method on the `RPC module` without having to spin a server up. + /// + /// The params must be serializable as JSON array, see [`ToRpcParams`] for further documentation. + pub async fn call_with(&self, method: &str, params: Params) -> Option { + let params = params.to_rpc_params().ok(); + self.call(method, params).await + } + /// Helper alternative to `execute`, useful for writing unit tests without having to spin /// a server up. pub async fn call(&self, method: &str, params: Option>) -> Option { @@ -540,6 +549,8 @@ fn subscription_closed_err(sub_id: u64) -> Error { #[cfg(test)] mod tests { use super::*; + use serde_json::json; + #[test] fn rpc_modules_with_different_contexts_can_be_merged() { let cx = Vec::::new(); @@ -574,4 +585,64 @@ mod tests { assert!(module.method("hello_world").is_some()); assert!(module.method("hello_foobar").is_some()); } + + #[tokio::test] + async fn calling_method_without_server() { + // Call sync method with no params + let mut module = RpcModule::new(()); + module.register_method("boo", |_: RpcParams, _| Ok(String::from("boo!"))).unwrap(); + let result = module.call_with("boo", (None::<()>,)).await.unwrap(); + assert_eq!(result.as_ref(), String::from(r#"{"jsonrpc":"2.0","result":"boo!","id":0}"#)); + + // Call sync method with params + module + .register_method("foo", |params, _| { + let n: u16 = params.one().expect("valid params please"); + Ok(n * 2) + }) + .unwrap(); + let result = module.call_with("foo", [3]).await.unwrap(); + assert_eq!(result.as_ref(), String::from(r#"{"jsonrpc":"2.0","result":6,"id":0}"#)); + + // Call async method with params and context + struct MyContext; + impl MyContext { + fn roo(&self, things: Vec) -> u16 { + things.iter().sum::().into() + } + } + let mut module = RpcModule::new(MyContext); + module + .register_async_method("roo", |params, ctx| { + let ns: Vec = params.parse().expect("valid params please"); + async move { Ok(ctx.roo(ns)) }.boxed() + }) + .unwrap(); + + module + .register_async_method("many_args", |params, _ctx| { + let mut seq = params.sequence(); + + let one = seq.next::>().unwrap().iter().sum::(); + let two = seq.optional_next::>().unwrap().unwrap_or_default().iter().sum::(); + let three: usize = seq.optional_next::>().unwrap().unwrap_or_default().iter().sum::(); + + let res = one + two + three; + + async move { Ok(res) }.boxed() + }) + .unwrap(); + + let result = &module.call_with("roo", [12, 13]).await.unwrap(); + assert_eq!(result.as_ref(), String::from(r#"{"jsonrpc":"2.0","result":25,"id":0}"#)); + + let result = module.call_with("many_args", vec![vec![1, 3, 7]]).await.unwrap(); + assert_eq!(result.as_ref(), String::from(r#"{"jsonrpc":"2.0","result":11,"id":0}"#)); + + let result = module.call_with("many_args", vec![json!([1]), json!([2]), json!([3])]).await.unwrap(); + assert_eq!(result.as_ref(), String::from(r#"{"jsonrpc":"2.0","result":6,"id":0}"#)); + + let result = module.call_with("many_args", vec![&[1], &[2]]).await.unwrap(); + assert_eq!(result.as_ref(), String::from(r#"{"jsonrpc":"2.0","result":3,"id":0}"#)); + } }