diff --git a/examples/examples/proc_macro_bounds.rs b/examples/examples/proc_macro_bounds.rs new file mode 100644 index 0000000000..6e4ab6daaa --- /dev/null +++ b/examples/examples/proc_macro_bounds.rs @@ -0,0 +1,90 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use std::net::SocketAddr; + +use jsonrpsee::core::{async_trait, Error}; +use jsonrpsee::proc_macros::rpc; +use jsonrpsee::ws_client::WsClientBuilder; +use jsonrpsee::ws_server::{WsServerBuilder, WsServerHandle}; + +type ExampleHash = [u8; 32]; + +pub trait Config { + type Hash: Send + Sync + 'static; +} + +impl Config for ExampleHash { + type Hash = Self; +} + +/// The RPC macro requires `DeserializeOwned` for output types for the client implementation, while the +/// server implementation requires output types to be bounded by `Serialize`. +/// +/// In this example, we don't want the `Conf` to be bounded by default to +/// `Conf : Send + Sync + 'static + jsonrpsee::core::DeserializeOwned` for client implementation and +/// `Conf : Send + Sync + 'static + jsonrpsee::core::Serialize` for server implementation. +/// +/// Explicitly, specify client and server bounds to handle the `Serialize` and `DeserializeOwned` cases +/// just for the `Conf::hash` part. +#[rpc(server, client, namespace = "foo", client_bounds(T::Hash: jsonrpsee::core::DeserializeOwned), server_bounds(T::Hash: jsonrpsee::core::Serialize))] +pub trait Rpc { + #[method(name = "bar")] + fn method(&self) -> Result; +} + +pub struct RpcServerImpl; + +#[async_trait] +impl RpcServer for RpcServerImpl { + fn method(&self) -> Result<::Hash, Error> { + Ok([0u8; 32]) + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init() + .expect("setting default subscriber failed"); + + let (server_addr, _handle) = run_server().await?; + let url = format!("ws://{}", server_addr); + + let client = WsClientBuilder::default().build(&url).await?; + assert_eq!(RpcClient::::method(&client).await.unwrap(), [0u8; 32]); + + Ok(()) +} + +async fn run_server() -> anyhow::Result<(SocketAddr, WsServerHandle)> { + let server = WsServerBuilder::default().build("127.0.0.1:0").await?; + + let addr = server.local_addr()?; + let handle = server.start(RpcServerImpl.into_rpc())?; + Ok((addr, handle)) +} diff --git a/proc-macros/src/helpers.rs b/proc-macros/src/helpers.rs index 0c4f1991ed..cf6c7b92b7 100644 --- a/proc-macros/src/helpers.rs +++ b/proc-macros/src/helpers.rs @@ -30,7 +30,7 @@ use crate::visitor::{FindAllParams, FindSubscriptionParams}; use proc_macro2::{Span, TokenStream as TokenStream2}; use proc_macro_crate::{crate_name, FoundCrate}; use quote::quote; -use syn::{parse_quote, punctuated::Punctuated, visit::Visit, Token}; +use syn::{parse_quote, punctuated::Punctuated, token::Comma, visit::Visit, Token, WherePredicate}; /// Search for client-side `jsonrpsee` in `Cargo.toml`. pub(crate) fn find_jsonrpsee_client_crate() -> Result { @@ -91,10 +91,21 @@ pub(crate) fn generate_where_clause( item_trait: &syn::ItemTrait, sub_tys: &[syn::Type], is_client: bool, + bounds: Option<&Punctuated>, ) -> Vec { let visitor = visit_trait(item_trait, sub_tys); let additional_where_clause = item_trait.generics.where_clause.clone(); + if let Some(custom_bounds) = bounds { + let mut bounds = additional_where_clause + .map(|where_clause| where_clause.predicates.into_iter().collect()) + .unwrap_or(Vec::new()); + + bounds.extend(custom_bounds.iter().cloned()); + + return bounds; + } + item_trait .generics .type_params() diff --git a/proc-macros/src/lib.rs b/proc-macros/src/lib.rs index 0b0b21cae3..3654b2fb3e 100644 --- a/proc-macros/src/lib.rs +++ b/proc-macros/src/lib.rs @@ -145,6 +145,10 @@ pub(crate) mod visitor; /// implementation's methods conveniently. /// - `namespace`: add a prefix to all the methods and subscriptions in this RPC. For example, with namespace `foo` and /// method `spam`, the resulting method name will be `foo_spam`. +/// - `server_bounds`: replace *all* auto-generated trait bounds with the user-defined ones for the server +/// implementation. +/// - `client_bounds`: replace *all* auto-generated trait bounds with the user-defined ones for the client +/// implementation. /// /// **Trait requirements:** /// diff --git a/proc-macros/src/render_client.rs b/proc-macros/src/render_client.rs index cf32fce5cb..64348242ef 100644 --- a/proc-macros/src/render_client.rs +++ b/proc-macros/src/render_client.rs @@ -36,7 +36,7 @@ impl RpcDescription { let sub_tys: Vec = 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 where_clause = generate_where_clause(&self.trait_def, &sub_tys, true, self.client_bounds.as_ref()); let type_idents = self.trait_def.generics.type_params().collect::>(); let (impl_generics, type_generics, _) = self.trait_def.generics.split_for_impl(); @@ -63,7 +63,7 @@ impl RpcDescription { #(#sub_impls)* } - impl #trait_name #type_generics for T where T: #super_trait #(,#where_clause)* {} + impl #trait_name #type_generics for TypeJsonRpseeInteral where TypeJsonRpseeInteral: #super_trait #(,#where_clause)* {} }; Ok(trait_impl) diff --git a/proc-macros/src/render_server.rs b/proc-macros/src/render_server.rs index e59b5e2d56..e4ff1c4c5e 100644 --- a/proc-macros/src/render_server.rs +++ b/proc-macros/src/render_server.rs @@ -287,7 +287,7 @@ impl RpcDescription { and adds them into a single `RpcModule`."; let sub_tys: Vec = self.subscriptions.clone().into_iter().map(|s| s.item).collect(); - let where_clause = generate_where_clause(&self.trait_def, &sub_tys, false); + let where_clause = generate_where_clause(&self.trait_def, &sub_tys, false, self.server_bounds.as_ref()); // NOTE(niklasad1): empty where clause is valid rust syntax. Ok(quote! { diff --git a/proc-macros/src/rpc_macro.rs b/proc-macros/src/rpc_macro.rs index 8418740a0b..ceaaa89589 100644 --- a/proc-macros/src/rpc_macro.rs +++ b/proc-macros/src/rpc_macro.rs @@ -220,20 +220,41 @@ pub struct RpcDescription { pub(crate) methods: Vec, /// List of RPC subscriptions defined in the trait. pub(crate) subscriptions: Vec, + /// Optional user defined trait bounds for the client implementation. + pub(crate) client_bounds: Option>, + /// Optional user defined trait bounds for the server implementation. + pub(crate) server_bounds: Option>, } impl RpcDescription { pub fn from_item(attr: Attribute, mut item: syn::ItemTrait) -> syn::Result { - let [client, server, namespace] = AttributeMeta::parse(attr)?.retain(["client", "server", "namespace"])?; + let [client, server, namespace, client_bounds, server_bounds] = + AttributeMeta::parse(attr)?.retain(["client", "server", "namespace", "client_bounds", "server_bounds"])?; let needs_server = optional(server, Argument::flag)?.is_some(); let needs_client = optional(client, Argument::flag)?.is_some(); let namespace = optional(namespace, Argument::string)?; + let client_bounds = optional(client_bounds, Argument::group)?; + let server_bounds = optional(server_bounds, Argument::group)?; if !needs_server && !needs_client { return Err(syn::Error::new_spanned(&item.ident, "Either 'server' or 'client' attribute must be applied")); } + if client_bounds.is_some() && !needs_client { + return Err(syn::Error::new_spanned( + &item.ident, + "Attribute 'client' must be specified with 'client_bounds'", + )); + } + + if server_bounds.is_some() && !needs_server { + return Err(syn::Error::new_spanned( + &item.ident, + "Attribute 'server' must be specified with 'server_bounds'", + )); + } + let jsonrpsee_client_path = crate::helpers::find_jsonrpsee_client_crate().ok(); let jsonrpsee_server_path = crate::helpers::find_jsonrpsee_server_crate().ok(); @@ -313,6 +334,8 @@ impl RpcDescription { trait_def: item, methods, subscriptions, + client_bounds, + server_bounds, }) } diff --git a/proc-macros/tests/ui/correct/rpc_bounds.rs b/proc-macros/tests/ui/correct/rpc_bounds.rs new file mode 100644 index 0000000000..e299af0ffe --- /dev/null +++ b/proc-macros/tests/ui/correct/rpc_bounds.rs @@ -0,0 +1,90 @@ +//! Example of using proc macro to generate working client and server with bounds applied. + +use std::net::SocketAddr; + +use jsonrpsee::core::{async_trait, RpcResult}; +use jsonrpsee::proc_macros::rpc; +use jsonrpsee::ws_client::*; +use jsonrpsee::ws_server::WsServerBuilder; + +pub trait Config { + type Hash: Send + Sync + 'static; + type NotUsed; +} + +type ExampleHash = [u8; 32]; +impl Config for ExampleHash { + type Hash = Self; + type NotUsed = (); +} + +/// Client only RPC. +#[rpc(client, namespace = "foo", client_bounds(Conf::Hash: jsonrpsee::core::DeserializeOwned))] +pub trait MyRpcC { + #[method(name = "bar")] + fn method(&self) -> RpcResult; +} + +/// Server only RPC. +#[rpc(server, namespace = "foo", server_bounds(Conf::Hash: jsonrpsee::core::Serialize))] +pub trait MyRpcS { + #[method(name = "bar")] + fn method(&self) -> RpcResult; +} + +/// Client and server RPC. +#[rpc(server, client, namespace = "foo", client_bounds(Conf::Hash: jsonrpsee::core::DeserializeOwned), server_bounds(Conf::Hash: jsonrpsee::core::Serialize))] +pub trait MyRpcSC { + #[method(name = "bar")] + fn method(&self) -> RpcResult; +} + +/// Implementation for the `MyRpcS` trait (server only). +pub struct ServerOnlyImpl; +#[async_trait] +impl MyRpcSServer for ServerOnlyImpl { + fn method(&self) -> RpcResult<::Hash> { + Ok([0u8; 32]) + } +} + +/// Implementation for the `MyRpcSC` trait (client server rpc). +pub struct ServerClientServerImpl; +#[async_trait] +impl MyRpcSCServer for ServerClientServerImpl { + fn method(&self) -> RpcResult<::Hash> { + Ok([0u8; 32]) + } +} + +pub async fn websocket_servers() -> (SocketAddr, SocketAddr) { + // Start server from `MyRpcS` trait. + let server = WsServerBuilder::default().build("127.0.0.1:0").await.unwrap(); + let addr_server_only = server.local_addr().unwrap(); + server.start(ServerOnlyImpl.into_rpc()).unwrap(); + + // Start server from `MyRpcSC` trait. + let server = WsServerBuilder::default().build("127.0.0.1:0").await.unwrap(); + let addr_server_client = server.local_addr().unwrap(); + server.start(ServerClientServerImpl.into_rpc()).unwrap(); + + (addr_server_only, addr_server_client) +} + +#[tokio::main] +async fn main() { + let (server_addr, server_addr_w_client) = websocket_servers().await; + let (server_url, server_w_client_url) = (format!("ws://{}", server_addr), format!("ws://{}", server_addr_w_client)); + let client = WsClientBuilder::default().build(&server_url).await.unwrap(); + let client_second = WsClientBuilder::default().build(&server_w_client_url).await.unwrap(); + + // Use `MyRpcC` client to communicate to the `MyRpcS` server. + assert_eq!(MyRpcCClient::::method(&client).await.unwrap(), [0u8; 32]); + // Use `MyRpcC` client to communicate to the `MyRpcSC` server. + assert_eq!(MyRpcCClient::::method(&client_second).await.unwrap(), [0u8; 32]); + + // Use `MyRpcSC` client to communicate to the `MyRpcS` server. + assert_eq!(MyRpcCClient::::method(&client).await.unwrap(), [0u8; 32]); + // Use `MyRpcSC` client to communicate to the `MyRpcSC` server. + assert_eq!(MyRpcSCClient::::method(&client_second).await.unwrap(), [0u8; 32]); +} diff --git a/proc-macros/tests/ui/incorrect/rpc/rpc_bounds_without_impl.rs b/proc-macros/tests/ui/incorrect/rpc/rpc_bounds_without_impl.rs new file mode 100644 index 0000000000..df727436cf --- /dev/null +++ b/proc-macros/tests/ui/incorrect/rpc/rpc_bounds_without_impl.rs @@ -0,0 +1,19 @@ +use jsonrpsee::proc_macros::rpc; + +pub trait Config { + type Hash: Send + Sync + 'static; +} + +#[rpc(server, client_bounds(), server_bounds(Conf::Hash: jsonrpsee::core::Serialize))] +pub trait ClientBoundsForbidden { + #[method(name = "bar")] + fn method(&self) -> Result; +} + +#[rpc(client, server_bounds(), client_bounds(Conf::Hash: jsonrpsee::core::DeserializeOwned))] +pub trait ServerBoundsForbidden { + #[method(name = "bar")] + fn method(&self) -> Result; +} + +fn main() {} diff --git a/proc-macros/tests/ui/incorrect/rpc/rpc_bounds_without_impl.stderr b/proc-macros/tests/ui/incorrect/rpc/rpc_bounds_without_impl.stderr new file mode 100644 index 0000000000..30b3ed6aa2 --- /dev/null +++ b/proc-macros/tests/ui/incorrect/rpc/rpc_bounds_without_impl.stderr @@ -0,0 +1,11 @@ +error: Attribute 'client' must be specified with 'client_bounds' + --> tests/ui/incorrect/rpc/rpc_bounds_without_impl.rs:8:11 + | +8 | pub trait ClientBoundsForbidden { + | ^^^^^^^^^^^^^^^^^^^^^ + +error: Attribute 'server' must be specified with 'server_bounds' + --> tests/ui/incorrect/rpc/rpc_bounds_without_impl.rs:14:11 + | +14 | pub trait ServerBoundsForbidden { + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/proc-macros/tests/ui/incorrect/rpc/rpc_empty_bounds.rs b/proc-macros/tests/ui/incorrect/rpc/rpc_empty_bounds.rs new file mode 100644 index 0000000000..f4fcdce23c --- /dev/null +++ b/proc-macros/tests/ui/incorrect/rpc/rpc_empty_bounds.rs @@ -0,0 +1,16 @@ +use jsonrpsee::proc_macros::rpc; +use jsonrpsee::core::Error; + +pub trait Config { + type Hash: Send + Sync + 'static; +} + +/// Client bound must be `Conf::Hash: jsonrpsee::core::DeserializeOwned` +/// Server bound must be `Conf::Hash: jsonrpsee::core::Serialize` +#[rpc(server, client, namespace = "foo", client_bounds(), server_bounds())] +pub trait EmptyBounds { + #[method(name = "bar")] + fn method(&self) -> Result; +} + +fn main() {} diff --git a/proc-macros/tests/ui/incorrect/rpc/rpc_empty_bounds.stderr b/proc-macros/tests/ui/incorrect/rpc/rpc_empty_bounds.stderr new file mode 100644 index 0000000000..ec2f0f0892 --- /dev/null +++ b/proc-macros/tests/ui/incorrect/rpc/rpc_empty_bounds.stderr @@ -0,0 +1,26 @@ +error[E0277]: the trait bound `::Hash: Serialize` is not satisfied + --> tests/ui/incorrect/rpc/rpc_empty_bounds.rs:10:1 + | +10 | #[rpc(server, client, namespace = "foo", client_bounds(), server_bounds())] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Serialize` is not implemented for `::Hash` + | +note: required by a bound in `RpcModule::::register_method` + --> $WORKSPACE/core/src/server/rpc_module.rs + | + | R: Serialize, + | ^^^^^^^^^ required by this bound in `RpcModule::::register_method` + = note: this error originates in the attribute macro `rpc` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `for<'de> ::Hash: Deserialize<'de>` is not satisfied + --> tests/ui/incorrect/rpc/rpc_empty_bounds.rs:10:1 + | +10 | #[rpc(server, client, namespace = "foo", client_bounds(), server_bounds())] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `for<'de> Deserialize<'de>` is not implemented for `::Hash` + | + = note: required because of the requirements on the impl of `DeserializeOwned` for `::Hash` +note: required by a bound in `request` + --> $WORKSPACE/core/src/client/mod.rs + | + | R: DeserializeOwned; + | ^^^^^^^^^^^^^^^^ required by this bound in `request` + = note: this error originates in the attribute macro `rpc` (in Nightly builds, run with -Z macro-backtrace for more info)