diff --git a/proc-macros/src/attributes.rs b/proc-macros/src/attributes.rs index c90ffd6141..ba28bf3718 100644 --- a/proc-macros/src/attributes.rs +++ b/proc-macros/src/attributes.rs @@ -40,6 +40,12 @@ pub(crate) struct Argument { pub tokens: TokenStream2, } +#[derive(Debug, Clone)] +pub enum ParamKind { + Array, + Map, +} + #[derive(Debug, Clone)] pub struct Resource { pub name: syn::LitStr, @@ -189,3 +195,14 @@ where { arg.ok().map(transform).transpose() } + +pub(crate) fn parse_param_kind(arg: Result) -> syn::Result { + let kind: Option = optional(arg, Argument::value)?; + + match kind { + None => Ok(ParamKind::Array), + Some(ident) if ident == "array" => Ok(ParamKind::Array), + Some(ident) if ident == "map" => Ok(ParamKind::Map), + ident => Err(Error::new(ident.span(), "param_kind must be either `map` or `array`")), + } +} diff --git a/proc-macros/src/lib.rs b/proc-macros/src/lib.rs index ad913d73db..b5f1346af5 100644 --- a/proc-macros/src/lib.rs +++ b/proc-macros/src/lib.rs @@ -164,6 +164,7 @@ pub(crate) mod visitor; /// - `name` (mandatory): name of the RPC method. Does not have to be the same as the Rust method name. /// - `aliases`: list of name aliases for the RPC method as a comma separated string. /// - `blocking`: when set method execution will always spawn on a dedicated thread. Only usable with non-`async` methods. +/// - `param_kind`: kind of structure to use for parameter passing. Can be "array" or "map", defaults to "array". /// /// **Method requirements:** /// @@ -180,6 +181,7 @@ pub(crate) mod visitor; /// - `name` (mandatory): name of the RPC method. Does not have to be the same as the Rust method name. /// - `unsub` (mandatory): name of the RPC method to unsubscribe from the subscription. Must not be the same as `name`. /// - `item` (mandatory): type of items yielded by the subscription. Note that it must be the type, not string. +/// - `param_kind`: kind of structure to use for parameter passing. Can be "array" or "map", defaults to "array". /// /// **Method requirements:** /// diff --git a/proc-macros/src/render_client.rs b/proc-macros/src/render_client.rs index 3cda7daa4f..11a12680bf 100644 --- a/proc-macros/src/render_client.rs +++ b/proc-macros/src/render_client.rs @@ -23,12 +23,12 @@ // 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 crate::attributes::ParamKind; 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; +use syn::{FnArg, Pat, PatIdent, PatType, TypeParam}; impl RpcDescription { pub(super) fn render_client(&self) -> Result { @@ -95,18 +95,7 @@ impl RpcDescription { }; // Encoded parameters for the request. - let parameters = if !method.params.is_empty() { - let serde_json = self.jrps_client_item(quote! { types::__reexports::serde_json }); - let params = method.params.iter().map(|(param, _param_type)| { - quote! { #serde_json::to_value(&#param)? } - }); - quote! { - Some(vec![ #(#params),* ].into()) - } - } else { - quote! { None } - }; - + let parameters = self.encode_params(&method.params, &method.param_kind, &method.signature); // Doc-comment to be associated with the method. let docs = &method.docs; @@ -138,18 +127,7 @@ impl RpcDescription { let returns = quote! { Result<#sub_type<#item>, #jrps_error> }; // Encoded parameters for the request. - let parameters = if !sub.params.is_empty() { - let serde_json = self.jrps_client_item(quote! { types::__reexports::serde_json }); - let params = sub.params.iter().map(|(param, _param_type)| { - quote! { #serde_json::to_value(&#param)? } - }); - quote! { - Some(vec![ #(#params),* ].into()) - } - } else { - quote! { None } - }; - + let parameters = self.encode_params(&sub.params, &sub.param_kind, &sub.signature); // Doc-comment to be associated with the method. let docs = &sub.docs; @@ -161,4 +139,58 @@ impl RpcDescription { }; Ok(method) } + + fn encode_params( + &self, + params: &Vec<(syn::PatIdent, syn::Type)>, + param_kind: &ParamKind, + signature: &syn::TraitItemMethod, + ) -> TokenStream2 { + if !params.is_empty() { + let serde_json = self.jrps_client_item(quote! { types::__reexports::serde_json }); + let params = params.iter().map(|(param, _param_type)| { + quote! { #serde_json::to_value(&#param)? } + }); + match param_kind { + ParamKind::Map => { + // Extract parameter names. + let param_names = extract_param_names(&signature.sig); + // Combine parameter names and values into tuples. + let params = param_names.iter().zip(params).map(|pair| { + let param = pair.0; + let value = pair.1; + quote! { (#param, #value) } + }); + quote! { + Some(types::v2::ParamsSer::Map( + std::collections::BTreeMap::<&str, #serde_json::Value>::from( + [#(#params),*] + ) + ) + ) + } + } + ParamKind::Array => { + quote! { + Some(vec![ #(#params),* ].into()) + } + } + } + } else { + quote! { None } + } + } +} + +fn extract_param_names(sig: &syn::Signature) -> Vec { + sig.inputs + .iter() + .filter_map(|param| match param { + FnArg::Typed(PatType { pat, .. }) => match &**pat { + Pat::Ident(PatIdent { ident, .. }) => Some(ident.to_string()), + _ => None, + }, + _ => None, + }) + .collect() } diff --git a/proc-macros/src/rpc_macro.rs b/proc-macros/src/rpc_macro.rs index 3f10f763cd..1ae2f5f05e 100644 --- a/proc-macros/src/rpc_macro.rs +++ b/proc-macros/src/rpc_macro.rs @@ -27,7 +27,7 @@ //! Declaration of the JSON RPC generator procedural macros. use crate::{ - attributes::{optional, Argument, AttributeMeta, MissingArgument, Resource}, + attributes::{optional, parse_param_kind, Argument, AttributeMeta, MissingArgument, ParamKind, Resource}, helpers::extract_doc_comments, }; @@ -42,6 +42,7 @@ pub struct RpcMethod { pub blocking: bool, pub docs: TokenStream2, pub params: Vec<(syn::PatIdent, syn::Type)>, + pub param_kind: ParamKind, pub returns: Option, pub signature: syn::TraitItemMethod, pub aliases: Vec, @@ -50,12 +51,13 @@ pub struct RpcMethod { impl RpcMethod { pub fn from_item(attr: Attribute, mut method: syn::TraitItemMethod) -> syn::Result { - let [aliases, blocking, name, resources] = - AttributeMeta::parse(attr)?.retain(["aliases", "blocking", "name", "resources"])?; + let [aliases, blocking, name, param_kind, resources] = + AttributeMeta::parse(attr)?.retain(["aliases", "blocking", "name", "param_kind", "resources"])?; let aliases = parse_aliases(aliases)?; let blocking = optional(blocking, Argument::flag)?.is_some(); let name = name?.string()?; + let param_kind = parse_param_kind(param_kind)?; let resources = optional(resources, Argument::group)?.unwrap_or_default(); let sig = method.sig.clone(); @@ -85,7 +87,7 @@ impl RpcMethod { // We've analyzed attributes and don't need them anymore. method.attrs.clear(); - Ok(Self { aliases, blocking, name, params, returns, signature: method, docs, resources }) + Ok(Self { aliases, blocking, name, params, param_kind, returns, signature: method, docs, resources }) } } @@ -95,6 +97,7 @@ pub struct RpcSubscription { pub docs: TokenStream2, pub unsubscribe: String, pub params: Vec<(syn::PatIdent, syn::Type)>, + pub param_kind: ParamKind, pub item: syn::Type, pub signature: syn::TraitItemMethod, pub aliases: Vec, @@ -103,12 +106,13 @@ pub struct RpcSubscription { impl RpcSubscription { pub fn from_item(attr: syn::Attribute, mut sub: syn::TraitItemMethod) -> syn::Result { - let [aliases, item, name, unsubscribe_aliases] = - AttributeMeta::parse(attr)?.retain(["aliases", "item", "name", "unsubscribe_aliases"])?; + let [aliases, item, name, param_kind, unsubscribe_aliases] = + AttributeMeta::parse(attr)?.retain(["aliases", "item", "name", "param_kind", "unsubscribe_aliases"])?; let aliases = parse_aliases(aliases)?; let name = name?.string()?; let item = item?.value()?; + let param_kind = parse_param_kind(param_kind)?; let unsubscribe_aliases = parse_aliases(unsubscribe_aliases)?; let sig = sub.sig.clone(); @@ -130,7 +134,7 @@ impl RpcSubscription { // We've analyzed attributes and don't need them anymore. sub.attrs.clear(); - Ok(Self { name, unsubscribe, unsubscribe_aliases, params, item, signature: sub, aliases, docs }) + Ok(Self { name, unsubscribe, unsubscribe_aliases, params, param_kind, item, signature: sub, aliases, docs }) } } diff --git a/proc-macros/tests/ui/correct/param_kind.rs b/proc-macros/tests/ui/correct/param_kind.rs new file mode 100644 index 0000000000..52c76ea7ac --- /dev/null +++ b/proc-macros/tests/ui/correct/param_kind.rs @@ -0,0 +1,63 @@ +use jsonrpsee::{ + proc_macros::rpc, + types::{async_trait, RpcResult}, + ws_client::*, + ws_server::WsServerBuilder, +}; + +use std::net::SocketAddr; + +#[rpc(client, server, namespace = "foo")] +pub trait Rpc { + #[method(name = "method_with_array_param", param_kind = array)] + async fn method_with_array_param(&self, param_a: u8, param_b: String) -> RpcResult; + + #[method(name="method_with_map_param", param_kind= map)] + async fn method_with_map_param(&self, param_a: u8, param_b: String) -> RpcResult; + + #[method(name="method_with_default_param")] + async fn method_with_default_param(&self, param_a: u8, param_b: String) -> RpcResult; +} + +pub struct RpcServerImpl; + +#[async_trait] +impl RpcServer for RpcServerImpl { + async fn method_with_array_param(&self, param_a: u8, param_b: String) -> RpcResult { + assert_eq!(param_a, 0); + assert_eq!(¶m_b, "a"); + Ok(42u16) + } + + async fn method_with_map_param(&self, param_a: u8, param_b: String) -> RpcResult { + assert_eq!(param_a, 0); + assert_eq!(¶m_b, "a"); + Ok(42u16) + } + + async fn method_with_default_param(&self, param_a: u8, param_b: String) -> RpcResult { + assert_eq!(param_a, 0); + assert_eq!(¶m_b, "a"); + Ok(42u16) + } +} + +pub async fn websocket_server() -> SocketAddr { + let server = WsServerBuilder::default().build("127.0.0.1:0").await.unwrap(); + let addr = server.local_addr().unwrap(); + + server.start(RpcServerImpl.into_rpc()).unwrap(); + + addr +} + +#[tokio::main] +async fn main() { + let server_addr = websocket_server().await; + let server_url = format!("ws://{}", server_addr); + let client = WsClientBuilder::default().build(&server_url).await.unwrap(); + + assert_eq!(client.method_with_array_param(0, "a".into()).await.unwrap(), 42); + assert_eq!(client.method_with_map_param(0, "a".into()).await.unwrap(), 42); + assert_eq!(client.method_with_default_param(0, "a".into()).await.unwrap(), 42); +} diff --git a/proc-macros/tests/ui/incorrect/method/method_unexpected_field.stderr b/proc-macros/tests/ui/incorrect/method/method_unexpected_field.stderr index fccf3ba76c..81b031b034 100644 --- a/proc-macros/tests/ui/incorrect/method/method_unexpected_field.stderr +++ b/proc-macros/tests/ui/incorrect/method/method_unexpected_field.stderr @@ -1,5 +1,5 @@ -error: Unknown argument `magic`, expected one of: `aliases`, `blocking`, `name`, `resources` - --> $DIR/method_unexpected_field.rs:6:25 +error: Unknown argument `magic`, expected one of: `aliases`, `blocking`, `name`, `param_kind`, `resources` + --> tests/ui/incorrect/method/method_unexpected_field.rs:6:25 | 6 | #[method(name = "foo", magic = false)] | ^^^^^ diff --git a/proc-macros/tests/ui/incorrect/sub/sub_unsupported_field.stderr b/proc-macros/tests/ui/incorrect/sub/sub_unsupported_field.stderr index a6cced13f0..87e90136fe 100644 --- a/proc-macros/tests/ui/incorrect/sub/sub_unsupported_field.stderr +++ b/proc-macros/tests/ui/incorrect/sub/sub_unsupported_field.stderr @@ -1,5 +1,5 @@ -error: Unknown argument `magic`, expected one of: `aliases`, `item`, `name`, `unsubscribe_aliases` - --> $DIR/sub_unsupported_field.rs:6:42 +error: Unknown argument `magic`, expected one of: `aliases`, `item`, `name`, `param_kind`, `unsubscribe_aliases` + --> tests/ui/incorrect/sub/sub_unsupported_field.rs:6:42 | 6 | #[subscription(name = "sub", item = u8, magic = true)] | ^^^^^