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

[proc macros]: support generic type params #436

Merged
merged 34 commits into from
Aug 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
faf78f1
PoC support generic type params
niklasad1 Aug 20, 2021
351acaf
more annoying example
niklasad1 Aug 20, 2021
66640e7
add trait bounds for generic params in proc macros
niklasad1 Aug 20, 2021
1f91aac
add compile-time test for complicated trait
niklasad1 Aug 20, 2021
230613e
smarter trait bounds in proc macros
niklasad1 Aug 23, 2021
4ed5b58
add non-working example for now
niklasad1 Aug 24, 2021
f442612
revert nits
niklasad1 Aug 24, 2021
834990e
Update examples/proc_macro.rs
niklasad1 Aug 25, 2021
fbb6f00
Update proc-macros/src/helpers.rs
niklasad1 Aug 25, 2021
6e5bcf1
add messy code but works
niklasad1 Aug 25, 2021
ab0b2ec
cleanup
niklasad1 Aug 25, 2021
63c48bd
add some simple compile check in tests
niklasad1 Aug 25, 2021
c7ce163
Merge branch 'na-proc-macros-generics' of github.com:paritytech/jsonr…
niklasad1 Aug 25, 2021
c843a99
Merge remote-tracking branch 'origin/master' into na-proc-macros-gene…
niklasad1 Aug 25, 2021
f04aeaf
fix doc link
niklasad1 Aug 25, 2021
52f61b2
fix doc link last time
niklasad1 Aug 25, 2021
64b33fb
address grumbles
niklasad1 Aug 26, 2021
c9b00c8
docs
niklasad1 Aug 26, 2021
8bff2e9
Update proc-macros/src/helpers.rs
niklasad1 Aug 26, 2021
fbd9683
Update proc-macros/src/helpers.rs
niklasad1 Aug 26, 2021
9f0b300
Update proc-macros/src/helpers.rs
niklasad1 Aug 26, 2021
3065c04
Update proc-macros/src/helpers.rs
niklasad1 Aug 27, 2021
6971610
Update proc-macros/src/visitor.rs
niklasad1 Aug 27, 2021
6f3fed9
fix nit: | -> ||
niklasad1 Aug 27, 2021
6abda51
Update proc-macros/src/helpers.rs
niklasad1 Aug 27, 2021
d86752a
Update proc-macros/src/helpers.rs
niklasad1 Aug 27, 2021
4c83ecc
Update proc-macros/src/helpers.rs
niklasad1 Aug 27, 2021
6ad7bc2
add issues to introduced TODOs
niklasad1 Aug 27, 2021
f8facb2
generics support where clause on trait
niklasad1 Aug 27, 2021
e197af5
Update proc-macros/src/helpers.rs
niklasad1 Aug 27, 2021
27c3a15
Update proc-macros/src/helpers.rs
niklasad1 Aug 27, 2021
ebd7147
address grumbles
niklasad1 Aug 27, 2021
3626e15
Merge branch 'na-proc-macros-generics' of github.com:paritytech/jsonr…
niklasad1 Aug 27, 2021
ba52568
add more docs
niklasad1 Aug 27, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 27 additions & 18 deletions examples/proc_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,43 @@

use jsonrpsee::{
proc_macros::rpc,
types::{async_trait, error::Error},
types::{async_trait, error::Error, Subscription},
ws_client::WsClientBuilder,
ws_server::{RpcModule, SubscriptionSink, WsServerBuilder},
ws_server::{SubscriptionSink, WsServerBuilder},
};
use std::net::SocketAddr;

type ExampleHash = [u8; 32];
type ExampleStorageKey = Vec<u8>;

#[rpc(server, client, namespace = "state")]
pub trait Rpc {
pub trait Rpc<Hash: Clone, StorageKey>
where
Hash: std::fmt::Debug,
{
/// Async method call example.
#[method(name = "getPairs")]
async fn storage_pairs(&self, prefix: usize, hash: Option<u128>) -> Result<Vec<usize>, Error>;
#[method(name = "getKeys")]
async fn storage_keys(&self, storage_key: StorageKey, hash: Option<Hash>) -> Result<Vec<StorageKey>, Error>;

/// Subscription that take `Option<Vec<u8>>` as input and produces output `Vec<usize>`.
#[subscription(name = "subscribeStorage", unsub = "unsubscribeStorage", item = Vec<usize>)]
fn subscribe_storage(&self, keys: Option<Vec<u8>>);
/// Subscription that takes a `StorageKey` as input and produces a `Vec<Hash>`.
#[subscription(name = "subscribeStorage", unsub = "unsubscribeStorage", item = Vec<Hash>)]
fn subscribe_storage(&self, keys: Option<Vec<StorageKey>>);
}

pub struct RpcServerImpl;

#[async_trait]
impl RpcServer for RpcServerImpl {
async fn storage_pairs(&self, _prefix: usize, _hash: Option<u128>) -> Result<Vec<usize>, Error> {
Ok(vec![1, 2, 3, 4])
impl RpcServer<ExampleHash, ExampleStorageKey> for RpcServerImpl {
async fn storage_keys(
&self,
storage_key: ExampleStorageKey,
_hash: Option<ExampleHash>,
) -> Result<Vec<ExampleStorageKey>, Error> {
Ok(vec![storage_key])
}

fn subscribe_storage(&self, mut sink: SubscriptionSink, keys: Option<Vec<u8>>) {
sink.send(&keys.unwrap_or_default()).unwrap();
fn subscribe_storage(&self, mut sink: SubscriptionSink, _keys: Option<Vec<ExampleStorageKey>>) {
sink.send(&vec![[0; 32]]).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe produce a few more items here?

Copy link
Member Author

Choose a reason for hiding this comment

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

technically, we don't have any compile time check that actually only Vec<Hash> can sent over the sink because send has T: Serialize bound.... not sure if we want ensure that at compile-time

}
}

Expand All @@ -64,18 +74,17 @@ async fn main() -> anyhow::Result<()> {
let url = format!("ws://{}", server_addr);

let client = WsClientBuilder::default().build(&url).await?;
assert_eq!(client.storage_pairs(10, None).await.unwrap(), vec![1, 2, 3, 4]);
assert_eq!(client.storage_keys(vec![1, 2, 3, 4], None::<ExampleHash>).await.unwrap(), vec![vec![1, 2, 3, 4]]);

let mut sub = client.subscribe_storage(None).await.unwrap();
assert_eq!(Some(vec![]), sub.next().await.unwrap());
let mut sub: Subscription<Vec<ExampleHash>> =
RpcClient::<ExampleHash, ExampleStorageKey>::subscribe_storage(&client, None).await.unwrap();
Copy link
Member Author

Choose a reason for hiding this comment

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

annoying to have to add these type hints but I think the issue for that is that we implement the client trait for T and these are not known in this cause at least not ExampleStorageKey ^^

assert_eq!(Some(vec![[0; 32]]), sub.next().await.unwrap());

Ok(())
}

async fn run_server() -> anyhow::Result<SocketAddr> {
let server = WsServerBuilder::default().build("127.0.0.1:0").await?;
let mut module = RpcModule::new(());
module.register_method("state_getPairs", |_, _| Ok(vec![1, 2, 3]))?;

let addr = server.local_addr()?;
tokio::spawn(async move { server.start(RpcServerImpl.into_rpc()).await });
Expand Down
2 changes: 1 addition & 1 deletion proc-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ proc-macro = true
Inflector = "0.11.4"
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0", default-features = false, features = ["extra-traits", "full"] }
syn = { version = "1.0", default-features = false, features = ["extra-traits", "full", "visit", "parsing"] }
proc-macro-crate = "1"
bae = "0.1.6"

Expand Down
92 changes: 92 additions & 0 deletions proc-macros/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

use crate::visitor::{FindAllParams, FindSubscriptionParams};
use proc_macro2::Span;
use proc_macro_crate::{crate_name, FoundCrate};
use quote::quote;
use std::collections::HashSet;
use syn::{parse_quote, punctuated::Punctuated, visit::Visit, Token};

/// Search for client-side `jsonrpsee` in `Cargo.toml`.
pub(crate) fn find_jsonrpsee_client_crate() -> Result<proc_macro2::TokenStream, syn::Error> {
Expand Down Expand Up @@ -57,3 +60,92 @@ fn find_jsonrpsee_crate(http_name: &str, ws_name: &str) -> Result<proc_macro2::T
},
}
}

/// Traverses the RPC trait definition and applies the required bounds for the generic type parameters that are used.
/// The bounds applied depend on whether the type parameter is used as a parameter, return value or subscription result
/// and whether it's used in client or server mode.
/// Type params get `Send + Sync + 'static` bounds and input/output parameters get `Serialize` and/or `DeserializeOwned` bounds.
/// Inspired by <https://github.com/paritytech/jsonrpc/blob/master/derive/src/to_delegate.rs#L414>
///
/// ### Example
///
/// ```
/// use jsonrpsee::{proc_macros::rpc, types::JsonRpcResult};
///
/// #[rpc(client, server)]
/// pub trait RpcTrait<A, B, C> {
/// #[method(name = "call")]
/// fn call(&self, a: A) -> JsonRpcResult<B>;
///
/// #[subscription(name = "sub", unsub = "unsub", item = Vec<C>)]
/// fn sub(&self);
/// }
/// ```
///
/// Because the `item` attribute is not parsed as ordinary rust syntax, the `syn::Type` is traversed to find
/// each generic parameter of it.
/// This is used as an additional input before traversing the entire trait.
/// Otherwise, it's not possible to know whether a type parameter is used for subscription result.
pub(crate) fn generate_where_clause(
item_trait: &syn::ItemTrait,
sub_tys: &[syn::Type],
is_client: bool,
) -> Vec<syn::WherePredicate> {
let visitor = visit_trait(item_trait, sub_tys);
let additional_where_clause = item_trait.generics.where_clause.clone();

item_trait
.generics
.type_params()
.map(|ty| {
let ty_path = syn::TypePath { qself: None, path: ty.ident.clone().into() };
let mut bounds: Punctuated<syn::TypeParamBound, Token![+]> = parse_quote!(Send + Sync + 'static);

if is_client {
if visitor.input_params.contains(&ty.ident) {
bounds.push(parse_quote!(jsonrpsee::types::Serialize))
}
if visitor.ret_params.contains(&ty.ident) || visitor.sub_params.contains(&ty.ident) {
bounds.push(parse_quote!(jsonrpsee::types::DeserializeOwned))
}
} else {
if visitor.input_params.contains(&ty.ident) {
bounds.push(parse_quote!(jsonrpsee::types::DeserializeOwned))
}
if visitor.ret_params.contains(&ty.ident) || visitor.sub_params.contains(&ty.ident) {
bounds.push(parse_quote!(jsonrpsee::types::Serialize))
}
}

// Add the trait bounds specified in the trait.
if let Some(where_clause) = &additional_where_clause {
for predicate in where_clause.predicates.iter() {
if let syn::WherePredicate::Type(where_ty) = predicate {
if let syn::Type::Path(ref predicate) = where_ty.bounded_ty {
if *predicate == ty_path {
bounds.extend(where_ty.bounds.clone().into_iter());
}
}
}
}
}

syn::WherePredicate::Type(syn::PredicateType {
lifetimes: None,
bounded_ty: syn::Type::Path(ty_path),
colon_token: <Token![:]>::default(),
bounds,
})
})
.collect()
}

/// Traverse the RPC trait by first finding the subscription parameters and then all elements
/// needed for generating the `client` and `server` traits/implementations.
fn visit_trait(item_trait: &syn::ItemTrait, sub_tys: &[syn::Type]) -> FindAllParams {
let type_params: HashSet<_> = item_trait.generics.type_params().map(|t| t.ident.clone()).collect();
let sub_tys = FindSubscriptionParams::new(type_params).visit(sub_tys);
let mut visitor = FindAllParams::new(sub_tys);
visitor.visit_item_trait(item_trait);
visitor
}
1 change: 1 addition & 0 deletions proc-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ mod render_client;
mod render_server;
mod respan;
mod rpc_macro;
pub(crate) mod visitor;

/// Main RPC macro.
///
Expand Down
14 changes: 9 additions & 5 deletions proc-macros/src/render_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,21 @@
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

use crate::rpc_macro::{RpcMethod, RpcSubscription};
use crate::RpcDescription;

use crate::helpers::generate_where_clause;
use crate::rpc_macro::{RpcDescription, RpcMethod, RpcSubscription};
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::TypeParam;

impl RpcDescription {
pub(super) fn render_client(&self) -> Result<TokenStream2, syn::Error> {
let jsonrpsee = self.jsonrpsee_client_path.as_ref().unwrap();
let sub_tys: Vec<syn::Type> = self.subscriptions.clone().into_iter().map(|s| s.item).collect();

let trait_name = quote::format_ident!("{}Client", &self.trait_def.ident);
let where_clause = generate_where_clause(&self.trait_def, &sub_tys, true);
let type_idents = self.trait_def.generics.type_params().collect::<Vec<&TypeParam>>();
let (impl_generics, type_generics, _) = self.trait_def.generics.split_for_impl();

let super_trait = if self.subscriptions.is_empty() {
quote! { #jsonrpsee::types::traits::Client }
Expand All @@ -54,12 +58,12 @@ impl RpcDescription {
let trait_impl = quote! {
#[#async_trait]
#[doc = #doc_comment]
pub trait #trait_name: #super_trait {
pub trait #trait_name #impl_generics: #super_trait where #(#where_clause,)* {
#(#method_impls)*
#(#sub_impls)*
}

impl<T> #trait_name for T where T: #super_trait {}
impl<T #(,#type_idents)*> #trait_name #type_generics for T where T: #super_trait #(,#where_clause)* {}
};

Ok(trait_impl)
Expand Down
20 changes: 14 additions & 6 deletions proc-macros/src/render_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@

use super::lifetimes::replace_lifetimes;
use super::RpcDescription;
use crate::helpers::generate_where_clause;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, quote_spanned};
use std::collections::HashSet;

impl RpcDescription {
pub(super) fn render_server(&self) -> Result<TokenStream2, syn::Error> {
let trait_name = quote::format_ident!("{}Server", &self.trait_def.ident);
let generics = self.trait_def.generics.clone();
let (impl_generics, _, where_clause) = generics.split_for_impl();

let method_impls = self.render_methods()?;
let into_rpc_impl = self.render_into_rpc()?;
Expand All @@ -45,7 +48,7 @@ impl RpcDescription {
let trait_impl = quote! {
#[#async_trait]
#[doc = #doc_comment]
pub trait #trait_name: Sized + Send + Sync + 'static {
pub trait #trait_name #impl_generics: Sized + Send + Sync + 'static #where_clause {
#method_impls
#into_rpc_impl
}
Expand Down Expand Up @@ -163,9 +166,13 @@ impl RpcDescription {
let doc_comment = "Collects all the methods and subscriptions defined in the trait \
and adds them into a single `RpcModule`.";

let sub_tys: Vec<syn::Type> = self.subscriptions.clone().into_iter().map(|s| s.item).collect();
let where_clause = generate_where_clause(&self.trait_def, &sub_tys, false);

// NOTE(niklasad1): empty where clause is valid rust syntax.
Ok(quote! {
#[doc = #doc_comment]
fn into_rpc(self) -> #rpc_module<Self> {
fn into_rpc(self) -> #rpc_module<Self> where #(#where_clause,)* {
let mut rpc = #rpc_module::new(self);

#(#errors)*
Expand Down Expand Up @@ -202,12 +209,11 @@ impl RpcDescription {
quote! {
let mut seq = params.sequence();
#(#decode_fields);*
(#params_fields)
}
};

// Code to decode sequence of parameters from a JSON object (aka map).
let decode_map = {
let _decode_map = {
let mut generics = None;

let serde = self.jrps_server_item(quote! { types::__reexports::serde });
Expand Down Expand Up @@ -246,11 +252,13 @@ impl RpcDescription {

// Parsing of `serde_json::Value`.
let parsing = quote! {
let (#params_fields) = if params.is_object() {
// TODO: https://github.com/paritytech/jsonrpsee/issues/445
/*let (#params_fields) = if params.is_object() {
#decode_map
} else {
#decode_array
};
};*/
#decode_array;
};

(parsing, params_fields)
Expand Down
3 changes: 2 additions & 1 deletion proc-macros/src/rpc_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ impl RpcSubscription {
let name = attributes.name;
let unsub_method = attributes.unsub;
let item = attributes.item;

let params: Vec<_> = sig
.inputs
.into_iter()
Expand Down Expand Up @@ -116,7 +117,7 @@ pub struct RpcDescription {
pub(crate) trait_def: syn::ItemTrait,
/// List of RPC methods defined in the trait.
pub(crate) methods: Vec<RpcMethod>,
/// List of RPC subscritpions defined in the trait.
/// List of RPC subscriptions defined in the trait.
pub(crate) subscriptions: Vec<RpcSubscription>,
}

Expand Down
Loading