Skip to content

Commit

Permalink
[rpc module] test helper for calling and converting types to JSON-RPC…
Browse files Browse the repository at this point in the history
… params (#458)

* new helper for testing

* fix features

* call_with_params -> call_with

* call_with_params -> call_with

* Update utils/src/server/rpc_module.rs

* fix trait bound

* Update utils/src/server/rpc_module.rs

Co-authored-by: David <dvdplm@gmail.com>

* call_with -> test_call

* fix grumbles

* fix nits

Co-authored-by: David <dvdplm@gmail.com>
  • Loading branch information
niklasad1 and dvdplm committed Sep 13, 2021
1 parent d452bfb commit 932304c
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 2 deletions.
56 changes: 56 additions & 0 deletions types/src/traits.rs
Expand Up @@ -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]
Expand Down Expand Up @@ -83,3 +85,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<Box<RawValue>, serde_json::Error> {
serde_json::to_string(&self).map(|json| RawValue::from_string(json).expect("JSON String; qed"))
}
}

impl<P: Serialize> ToRpcParams for &[P] {}

impl<P: Serialize> ToRpcParams for Vec<P> {}

macro_rules! array_impls {
($($len:tt)+) => {
$(
impl<P: Serialize> 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)
}
3 changes: 1 addition & 2 deletions utils/Cargo.toml
Expand Up @@ -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 }
Expand All @@ -35,8 +34,8 @@ server = [
"log",
"parking_lot",
"rand",
"tokio"
]

[dev-dependencies]
serde_json = "1.0"
tokio = { version = "1", features = ["macros", "rt"] }
71 changes: 71 additions & 0 deletions utils/src/server/rpc_module.rs
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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<Params: ToRpcParams>(&self, method: &str, params: Params) -> Option<String> {
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<Box<RawValue>>) -> Option<String> {
Expand Down Expand Up @@ -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::<u8>::new();
Expand Down Expand Up @@ -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<u8>) -> u16 {
things.iter().sum::<u8>().into()
}
}
let mut module = RpcModule::new(MyContext);
module
.register_async_method("roo", |params, ctx| {
let ns: Vec<u8> = 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::<Vec<usize>>().unwrap().iter().sum::<usize>();
let two = seq.optional_next::<Vec<usize>>().unwrap().unwrap_or_default().iter().sum::<usize>();
let three: usize = seq.optional_next::<Vec<usize>>().unwrap().unwrap_or_default().iter().sum::<usize>();

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}"#));
}
}

0 comments on commit 932304c

Please sign in to comment.