Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rpc module] test helper for calling and converting types to JSON-RPC params #458

Merged
merged 12 commits into from
Sep 13, 2021
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 @@ -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<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 {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const generics doesn't work because of serde I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, came here to ask about this.

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