Skip to content

Commit

Permalink
Proc mac support map param (#544)
Browse files Browse the repository at this point in the history
* feat(proc_macro): add support for map arguments

* feat(proc_macro): formatting

* feat(proc_macro): fix issues with Into trait

* feat(proc_macro): param_format for methods

* feat(proc_macro): improve param_format checking

- Addressed @niklasad1's suggestion to use an Option instead of just
defaulting to "array".

* feat(proc_macro): apply suggestions, add test case

- Use enum for param format.
- Extract parsing logic into separate function.
- Add ui test.

* feat(proc_macro): run cargo fmt

* feat(proc_macro): address suggestions

* feat(proc_macro): document param_kind argument

* feat(proc_macro):  consistent spacing

Apply @maciejhirsz formatting suggestion.

Co-authored-by: Maciej Hirsz <1096222+maciejhirsz@users.noreply.github.com>

* feat(proc_macro): apply suggestions

- make parameter encoding DRY
- remove strings from param_kind
- return result from parse_param_kind

* feat(proc_macro): formatting

Co-authored-by: Maciej Hirsz <1096222+maciejhirsz@users.noreply.github.com>
  • Loading branch information
DefinitelyNotHilbert and maciejhirsz authored Nov 3, 2021
1 parent 092081a commit ff3337b
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 37 deletions.
17 changes: 17 additions & 0 deletions proc-macros/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -189,3 +195,14 @@ where
{
arg.ok().map(transform).transpose()
}

pub(crate) fn parse_param_kind(arg: Result<Argument, MissingArgument>) -> syn::Result<ParamKind> {
let kind: Option<syn::Ident> = 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`")),
}
}
2 changes: 2 additions & 0 deletions proc-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
///
Expand All @@ -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:**
///
Expand Down
84 changes: 58 additions & 26 deletions proc-macros/src/render_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TokenStream2, syn::Error> {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand All @@ -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<String> {
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()
}
18 changes: 11 additions & 7 deletions proc-macros/src/rpc_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand All @@ -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<syn::Type>,
pub signature: syn::TraitItemMethod,
pub aliases: Vec<String>,
Expand All @@ -50,12 +51,13 @@ pub struct RpcMethod {

impl RpcMethod {
pub fn from_item(attr: Attribute, mut method: syn::TraitItemMethod) -> syn::Result<Self> {
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();
Expand Down Expand Up @@ -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 })
}
}

Expand All @@ -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<String>,
Expand All @@ -103,12 +106,13 @@ pub struct RpcSubscription {

impl RpcSubscription {
pub fn from_item(attr: syn::Attribute, mut sub: syn::TraitItemMethod) -> syn::Result<Self> {
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();
Expand All @@ -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 })
}
}

Expand Down
63 changes: 63 additions & 0 deletions proc-macros/tests/ui/correct/param_kind.rs
Original file line number Diff line number Diff line change
@@ -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<u16>;

#[method(name="method_with_map_param", param_kind= map)]
async fn method_with_map_param(&self, param_a: u8, param_b: String) -> RpcResult<u16>;

#[method(name="method_with_default_param")]
async fn method_with_default_param(&self, param_a: u8, param_b: String) -> RpcResult<u16>;
}

pub struct RpcServerImpl;

#[async_trait]
impl RpcServer for RpcServerImpl {
async fn method_with_array_param(&self, param_a: u8, param_b: String) -> RpcResult<u16> {
assert_eq!(param_a, 0);
assert_eq!(&param_b, "a");
Ok(42u16)
}

async fn method_with_map_param(&self, param_a: u8, param_b: String) -> RpcResult<u16> {
assert_eq!(param_a, 0);
assert_eq!(&param_b, "a");
Ok(42u16)
}

async fn method_with_default_param(&self, param_a: u8, param_b: String) -> RpcResult<u16> {
assert_eq!(param_a, 0);
assert_eq!(&param_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);
}
Original file line number Diff line number Diff line change
@@ -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)]
| ^^^^^
Original file line number Diff line number Diff line change
@@ -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)]
| ^^^^^

1 comment on commit ff3337b

@github-actions
Copy link

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 2.

Benchmark suite Current: ff3337b Previous: 092081a Ratio
subscriptions/unsub 2042 ns/iter (± 445) 895 ns/iter (± 208) 2.28

This comment was automatically generated by workflow using github-action-benchmark.

CC: @niklasad1 @maciejhirsz

Please sign in to comment.