diff --git a/core-client/transports/src/lib.rs b/core-client/transports/src/lib.rs index ab5a4bc6f..b6da7c7e2 100644 --- a/core-client/transports/src/lib.rs +++ b/core-client/transports/src/lib.rs @@ -303,9 +303,10 @@ impl TypedClient { let params = match args { Value::Array(vec) => Params::Array(vec), Value::Null => Params::None, + Value::Object(map) => Params::Map(map), _ => { return future::Either::A(future::err(RpcError::Other(format_err!( - "RPC params should serialize to a JSON array, or null" + "RPC params should serialize to a JSON array, JSON object or null" )))) } }; diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 71e6bb35e..8199f796a 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -15,7 +15,7 @@ proc-macro = true [dependencies] syn = { version = "1.0", features = ["full", "extra-traits", "visit", "fold"] } proc-macro2 = "1.0" -quote = "1.0" +quote = "=1.0.1" proc-macro-crate = "0.1.4" [dev-dependencies] diff --git a/derive/examples/meta-macros.rs b/derive/examples/meta-macros.rs index 58164e31a..8d7a64d7f 100644 --- a/derive/examples/meta-macros.rs +++ b/derive/examples/meta-macros.rs @@ -26,7 +26,7 @@ pub trait Rpc { fn mul(&self, a: u64, b: Option) -> Result; /// Retrieves and debug prints the underlying `Params` object. - #[rpc(name = "raw", raw_params)] + #[rpc(name = "raw", params = "raw")] fn raw(&self, params: Params) -> Result; /// Performs an asynchronous operation. diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 8fe096eb8..fa8d9fdfe 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -186,6 +186,7 @@ use proc_macro::TokenStream; use syn::parse_macro_input; mod options; +mod params_style; mod rpc_attr; mod rpc_trait; mod to_client; @@ -198,13 +199,14 @@ mod to_delegate; #[proc_macro_attribute] pub fn rpc(args: TokenStream, input: TokenStream) -> TokenStream { let input_toks = parse_macro_input!(input as syn::Item); + let args = syn::parse_macro_input!(args as syn::AttributeArgs); let options = match options::DeriveOptions::try_from(args) { Ok(options) => options, Err(error) => return error.to_compile_error().into(), }; - match rpc_trait::rpc_impl(input_toks, options) { + match rpc_trait::rpc_impl(input_toks, &options) { Ok(output) => output.into(), Err(err) => err.to_compile_error().into(), } diff --git a/derive/src/options.rs b/derive/src/options.rs index a6ba98ec0..fe52c07dc 100644 --- a/derive/src/options.rs +++ b/derive/src/options.rs @@ -1,37 +1,71 @@ -use proc_macro::TokenStream; +use std::str::FromStr; + +use crate::params_style::ParamStyle; +use crate::rpc_attr::path_eq_str; + +const CLIENT_META_WORD: &str = "client"; +const SERVER_META_WORD: &str = "server"; +const PARAMS_META_KEY: &str = "params"; #[derive(Debug)] pub struct DeriveOptions { pub enable_client: bool, pub enable_server: bool, + pub params_style: ParamStyle, } impl DeriveOptions { - pub fn new(enable_client: bool, enable_server: bool) -> Self { + pub fn new(enable_client: bool, enable_server: bool, params_style: ParamStyle) -> Self { DeriveOptions { enable_client, enable_server, + params_style, } } - pub fn try_from(tokens: TokenStream) -> Result { - if tokens.is_empty() { - return Ok(Self::new(true, true)); - } - let ident: syn::Ident = syn::parse::(tokens)?; - let options = { - let ident = ident.to_string(); - if ident == "client" { - Some(Self::new(true, false)) - } else if ident == "server" { - Some(Self::new(false, true)) - } else { - None + pub fn try_from(args: syn::AttributeArgs) -> Result { + let mut options = DeriveOptions::new(false, false, ParamStyle::default()); + for arg in args { + if let syn::NestedMeta::Meta(meta) = arg { + match meta { + syn::Meta::Path(ref p) => { + match p + .get_ident() + .ok_or(syn::Error::new_spanned( + p, + format!("Expecting identifier `{}` or `{}`", CLIENT_META_WORD, SERVER_META_WORD), + ))? + .to_string() + .as_ref() + { + CLIENT_META_WORD => options.enable_client = true, + SERVER_META_WORD => options.enable_server = true, + _ => {} + }; + } + syn::Meta::NameValue(nv) => { + if path_eq_str(&nv.path, PARAMS_META_KEY) { + if let syn::Lit::Str(ref lit) = nv.lit { + options.params_style = ParamStyle::from_str(&lit.value()) + .map_err(|e| syn::Error::new_spanned(nv.clone(), e))?; + } + } else { + return Err(syn::Error::new_spanned(nv, "Unexpected RPC attribute key")); + } + } + _ => return Err(syn::Error::new_spanned(meta, "Unexpected use of RPC attribute macro")), + } } - }; - match options { - Some(options) => Ok(options), - None => Err(syn::Error::new(ident.span(), "Unknown attribute.")), } + if !options.enable_client && !options.enable_server { + // if nothing provided default to both + options.enable_client = true; + options.enable_server = true; + } + if options.enable_server && options.params_style == ParamStyle::Named { + // This is not allowed at this time + panic!("Server code generation only supports `params = \"positional\"` (default) or `params = \"raw\" at this time.") + } + Ok(options) } } diff --git a/derive/src/params_style.rs b/derive/src/params_style.rs new file mode 100644 index 000000000..fe8b9dfe7 --- /dev/null +++ b/derive/src/params_style.rs @@ -0,0 +1,34 @@ +use std::str::FromStr; + +const POSITIONAL: &str = "positional"; +const NAMED: &str = "named"; +const RAW: &str = "raw"; + +#[derive(Clone, Debug, PartialEq)] +pub enum ParamStyle { + Positional, + Named, + Raw, +} + +impl Default for ParamStyle { + fn default() -> Self { + Self::Positional + } +} + +impl FromStr for ParamStyle { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + POSITIONAL => Ok(Self::Positional), + NAMED => Ok(Self::Named), + RAW => Ok(Self::Raw), + _ => Err(format!( + "Invalid value for params key. Must be one of [{}, {}, {}]", + POSITIONAL, NAMED, RAW + )), + } + } +} diff --git a/derive/src/rpc_attr.rs b/derive/src/rpc_attr.rs index 95a7c1286..215979d19 100644 --- a/derive/src/rpc_attr.rs +++ b/derive/src/rpc_attr.rs @@ -1,3 +1,5 @@ +use crate::params_style::ParamStyle; +use std::str::FromStr; use syn::{ visit::{self, Visit}, Error, Result, @@ -9,7 +11,7 @@ pub struct RpcMethodAttribute { pub name: String, pub aliases: Vec, pub kind: AttributeKind, - pub raw_params: bool, + pub params_style: Option, // None means do not override the top level default } #[derive(Clone, Debug)] @@ -37,10 +39,11 @@ const SUBSCRIPTION_NAME_KEY: &str = "subscription"; const ALIASES_KEY: &str = "alias"; const PUB_SUB_ATTR_NAME: &str = "pubsub"; const METADATA_META_WORD: &str = "meta"; -const RAW_PARAMS_META_WORD: &str = "raw_params"; +const RAW_PARAMS_META_WORD: &str = "raw_params"; // to be deprecated and replaced with `params = "raw"` const SUBSCRIBE_META_WORD: &str = "subscribe"; const UNSUBSCRIBE_META_WORD: &str = "unsubscribe"; const RETURNS_META_WORD: &str = "returns"; +const PARAMS_STYLE_KEY: &str = "params"; const MULTIPLE_RPC_ATTRIBUTES_ERR: &str = "Expected only a single rpc attribute per method"; const INVALID_ATTR_PARAM_NAMES_ERR: &str = "Invalid attribute parameter(s):"; @@ -81,12 +84,21 @@ impl RpcMethodAttribute { let aliases = get_meta_list(&meta).map_or(Vec::new(), |ml| get_aliases(ml)); let raw_params = get_meta_list(meta).map_or(false, |ml| has_meta_word(RAW_PARAMS_META_WORD, ml)); + let params_style = match raw_params { + true => { + // "`raw_params` will be deprecated in a future release. Use `params = \"raw\" instead`" + Ok(Some(ParamStyle::Raw)) + } + false => { + get_meta_list(meta).map_or(Ok(None), |ml| get_params_style(ml).map(|s| Some(s))) + } + }?; Ok(RpcMethodAttribute { attr: attr.clone(), name, aliases, kind, - raw_params, + params_style, }) }) }) @@ -179,7 +191,11 @@ fn validate_attribute_meta(meta: syn::Meta) -> Result { match ident.as_ref().map(String::as_str) { Some(RPC_ATTR_NAME) => { validate_idents(&meta, &visitor.meta_words, &[METADATA_META_WORD, RAW_PARAMS_META_WORD])?; - validate_idents(&meta, &visitor.name_value_names, &[RPC_NAME_KEY, RETURNS_META_WORD])?; + validate_idents( + &meta, + &visitor.name_value_names, + &[RPC_NAME_KEY, RETURNS_META_WORD, PARAMS_STYLE_KEY], + )?; validate_idents(&meta, &visitor.meta_list_names, &[ALIASES_KEY]) } Some(PUB_SUB_ATTR_NAME) => { @@ -279,7 +295,13 @@ fn get_aliases(ml: &syn::MetaList) -> Vec { }) } -fn path_eq_str(path: &syn::Path, s: &str) -> bool { +fn get_params_style(ml: &syn::MetaList) -> Result { + get_name_value(PARAMS_STYLE_KEY, ml).map_or(Ok(ParamStyle::default()), |s| { + ParamStyle::from_str(&s).map_err(|e| Error::new_spanned(ml, e)) + }) +} + +pub fn path_eq_str(path: &syn::Path, s: &str) -> bool { path.get_ident().map_or(false, |i| i == s) } diff --git a/derive/src/rpc_trait.rs b/derive/src/rpc_trait.rs index 737c18665..7550ade4f 100644 --- a/derive/src/rpc_trait.rs +++ b/derive/src/rpc_trait.rs @@ -1,4 +1,5 @@ use crate::options::DeriveOptions; +use crate::params_style::ParamStyle; use crate::rpc_attr::{AttributeKind, PubSubMethodKind, RpcMethodAttribute}; use crate::to_client::generate_client_module; use crate::to_delegate::{generate_trait_item_method, MethodRegistration, RpcMethod}; @@ -21,6 +22,10 @@ const MISSING_UNSUBSCRIBE_METHOD_ERR: &str = "Can't find unsubscribe method, expected a method annotated with `unsubscribe` \ e.g. `#[pubsub(subscription = \"hello\", unsubscribe, name = \"hello_unsubscribe\")]`"; +pub const USING_NAMED_PARAMS_WITH_SERVER_ERR: &str = + "`params = \"named\"` can only be used to generate a client (on a trait annotated with #[rpc(client)]). \ + At this time the server does not support named parameters."; + const RPC_MOD_NAME_PREFIX: &str = "rpc_impl_"; struct RpcTrait { @@ -218,13 +223,19 @@ fn rpc_wrapper_mod_name(rpc_trait: &syn::ItemTrait) -> syn::Ident { syn::Ident::new(&mod_name, proc_macro2::Span::call_site()) } +fn has_named_params(methods: &[RpcMethod]) -> bool { + methods + .iter() + .any(|method| method.attr.params_style == Some(ParamStyle::Named)) +} + pub fn crate_name(name: &str) -> Result { proc_macro_crate::crate_name(name) .map(|name| Ident::new(&name, Span::call_site())) .map_err(|e| Error::new(Span::call_site(), &e)) } -pub fn rpc_impl(input: syn::Item, options: DeriveOptions) -> Result { +pub fn rpc_impl(input: syn::Item, options: &DeriveOptions) -> Result { let rpc_trait = match input { syn::Item::Trait(item_trait) => item_trait, item => { @@ -245,13 +256,16 @@ pub fn rpc_impl(input: syn::Item, options: DeriveOptions) -> Result Result { - let client_methods = generate_client_methods(methods)?; +pub fn generate_client_module( + methods: &[MethodRegistration], + item_trait: &syn::ItemTrait, + options: &DeriveOptions, +) -> Result { + let client_methods = generate_client_methods(methods, &options)?; let generics = &item_trait.generics; let where_clause = generate_where_clause_serialization_predicates(&item_trait, true); let where_clause2 = where_clause.clone(); @@ -85,7 +91,7 @@ pub fn generate_client_module(methods: &[MethodRegistration], item_trait: &syn:: }) } -fn generate_client_methods(methods: &[MethodRegistration]) -> Result> { +fn generate_client_methods(methods: &[MethodRegistration], options: &DeriveOptions) -> Result> { let mut client_methods = vec![]; for method in methods { match method { @@ -100,11 +106,29 @@ fn generate_client_methods(methods: &[MethodRegistration]) -> Result continue, }; let returns_str = quote!(#returns).to_string(); + + let args_serialized = match method.attr.params_style.clone().unwrap_or(options.params_style.clone()) { + ParamStyle::Named => { + quote! { // use object style serialization with field names taken from the function param names + serde_json::json!({ + #(stringify!(#arg_names): #arg_names,)* + }) + } + } + ParamStyle::Positional => quote! { // use tuple style serialization + (#(#arg_names,)*) + }, + ParamStyle::Raw => match arg_names.first() { + Some(arg_name) => quote! {#arg_name}, + None => quote! {serde_json::Value::Null}, + }, + }; + let client_method = syn::parse_quote! { #(#attrs)* pub fn #name(&self, #args) -> impl Future { - let args_tuple = (#(#arg_names,)*); - self.inner.call_method(#rpc_name, #returns_str, args_tuple) + let args = #args_serialized; + self.inner.call_method(#rpc_name, #returns_str, args) } }; client_methods.push(client_method); diff --git a/derive/src/to_delegate.rs b/derive/src/to_delegate.rs index 81e801628..80c83ab31 100644 --- a/derive/src/to_delegate.rs +++ b/derive/src/to_delegate.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; +use crate::params_style::ParamStyle; use crate::rpc_attr::RpcMethodAttribute; use quote::quote; use syn::{ @@ -218,7 +219,6 @@ impl RpcMethod { // special args are those which are not passed directly via rpc params: metadata, subscriber let special_args = Self::special_args(¶m_types); param_types.retain(|ty| special_args.iter().find(|(_, sty)| sty == ty).is_none()); - if param_types.len() > TUPLE_FIELD_NAMES.len() { return Err(syn::Error::new_spanned( &self.trait_item, @@ -238,10 +238,14 @@ impl RpcMethod { self.params_with_trailing(trailing_args_num, param_types, tuple_fields) } else if param_types.is_empty() { quote! { let params = params.expect_no_params(); } - } else if self.attr.raw_params { + } else if self.attr.params_style == Some(ParamStyle::Raw) { quote! { let params: _jsonrpc_core::Result<_> = Ok((params,)); } - } else { + } else if self.attr.params_style == Some(ParamStyle::Positional) { quote! { let params = params.parse::<(#(#param_types, )*)>(); } + } else + /* if self.attr.params_style == Some(ParamStyle::Named) */ + { + unimplemented!("Server side named parameters are not implemented"); } }; diff --git a/derive/tests/client.rs b/derive/tests/client.rs index 469175cdf..f5eff5ee8 100644 --- a/derive/tests/client.rs +++ b/derive/tests/client.rs @@ -3,43 +3,133 @@ use jsonrpc_core::{IoHandler, Result}; use jsonrpc_core_client::transports::local; use jsonrpc_derive::rpc; -#[rpc] -pub trait Rpc { - #[rpc(name = "add")] - fn add(&self, a: u64, b: u64) -> Result; +mod client_server { + use super::*; - #[rpc(name = "notify")] - fn notify(&self, foo: u64); + #[rpc(params = "positional")] + pub trait Rpc { + #[rpc(name = "add")] + fn add(&self, a: u64, b: u64) -> Result; + + #[rpc(name = "notify")] + fn notify(&self, foo: u64); + } + + struct RpcServer; + + impl Rpc for RpcServer { + fn add(&self, a: u64, b: u64) -> Result { + Ok(a + b) + } + + fn notify(&self, foo: u64) { + println!("received {}", foo); + } + } + + #[test] + fn client_server_roundtrip() { + let mut handler = IoHandler::new(); + handler.extend_with(RpcServer.to_delegate()); + let (client, rpc_client) = local::connect::(handler); + let fut = client + .clone() + .add(3, 4) + .and_then(move |res| client.notify(res).map(move |_| res)) + .join(rpc_client) + .map(|(res, ())| { + assert_eq!(res, 7); + }) + .map_err(|err| { + eprintln!("{:?}", err); + assert!(false); + }); + tokio::run(fut); + } } -struct RpcServer; +mod named_params { + use super::*; + use jsonrpc_core::Params; + use serde_json::json; -impl Rpc for RpcServer { - fn add(&self, a: u64, b: u64) -> Result { - Ok(a + b) + #[rpc(client, params = "named")] + pub trait Rpc { + #[rpc(name = "call_with_named")] + fn call_with_named(&self, number: u64, string: String, json: Value) -> Result; + + #[rpc(name = "notify", params = "raw")] + fn notify(&self, payload: Value); } - fn notify(&self, foo: u64) { - println!("received {}", foo); + #[test] + fn client_generates_correct_named_params_payload() { + let expected = json!({ // key names are derived from function parameter names in the trait + "number": 3, + "string": String::from("test string"), + "json": { + "key": ["value"] + } + }); + + let mut handler = IoHandler::new(); + handler.add_method("call_with_named", |params: Params| Ok(params.into())); + + let (client, rpc_client) = local::connect::(handler); + let fut = client + .clone() + .call_with_named(3, String::from("test string"), json!({"key": ["value"]})) + .and_then(move |res| client.notify(res.clone()).map(move |_| res)) + .join(rpc_client) + .map(move |(res, ())| { + assert_eq!(res, expected); + }) + .map_err(|err| { + eprintln!("{:?}", err); + assert!(false); + }); + tokio::run(fut); } } -#[test] -fn client_server_roundtrip() { - let mut handler = IoHandler::new(); - handler.extend_with(RpcServer.to_delegate()); - let (client, rpc_client) = local::connect::(handler); - let fut = client - .clone() - .add(3, 4) - .and_then(move |res| client.notify(res).map(move |_| res)) - .join(rpc_client) - .map(|(res, ())| { - assert_eq!(res, 7); - }) - .map_err(|err| { - eprintln!("{:?}", err); - assert!(false); +mod raw_params { + use super::*; + use jsonrpc_core::Params; + use serde_json::json; + + #[rpc(client)] + pub trait Rpc { + #[rpc(name = "call_raw", params = "raw")] + fn call_raw_single_param(&self, params: Value) -> Result; + + #[rpc(name = "notify", params = "raw")] + fn notify(&self, payload: Value); + } + + #[test] + fn client_generates_correct_raw_params_payload() { + let expected = json!({ + "sub_object": { + "key": ["value"] + } }); - tokio::run(fut); + + let mut handler = IoHandler::new(); + handler.add_method("call_raw", |params: Params| Ok(params.into())); + + let (client, rpc_client) = local::connect::(handler); + let fut = client + .clone() + .call_raw_single_param(expected.clone()) + .and_then(move |res| client.notify(res.clone()).map(move |_| res)) + .join(rpc_client) + .map(move |(res, ())| { + assert_eq!(res, expected); + }) + .map_err(|err| { + eprintln!("{:?}", err); + assert!(false); + }); + tokio::run(fut); + } } diff --git a/derive/tests/macros.rs b/derive/tests/macros.rs index 632c27a8c..1f4483672 100644 --- a/derive/tests/macros.rs +++ b/derive/tests/macros.rs @@ -27,7 +27,7 @@ pub trait Rpc { fn add(&self, a: u64, b: u64) -> Result; /// Retrieves and debug prints the underlying `Params` object. - #[rpc(name = "raw", raw_params)] + #[rpc(name = "raw", params = "raw")] fn raw(&self, params: Params) -> Result; /// Handles a notification. diff --git a/derive/tests/ui/attr-invalid-name-values.stderr b/derive/tests/ui/attr-invalid-name-values.stderr index 9bb48a1c7..6811f0c3e 100644 --- a/derive/tests/ui/attr-invalid-name-values.stderr +++ b/derive/tests/ui/attr-invalid-name-values.stderr @@ -1,4 +1,4 @@ -error: Invalid attribute parameter(s): 'Xname'. Expected 'name, returns' +error: Invalid attribute parameter(s): 'Xname'. Expected 'name, returns, params' --> $DIR/attr-invalid-name-values.rs:5:2 | 5 | /// Returns a protocol version diff --git a/derive/tests/ui/attr-named-params-on-server.rs b/derive/tests/ui/attr-named-params-on-server.rs new file mode 100644 index 000000000..074995642 --- /dev/null +++ b/derive/tests/ui/attr-named-params-on-server.rs @@ -0,0 +1,10 @@ +use jsonrpc_derive::rpc; + +#[rpc] +pub trait Rpc { + /// Returns a protocol version + #[rpc(name = "add", params = "named")] + fn add(&self, a: u32, b: u32) -> Result; +} + +fn main() {} diff --git a/derive/tests/ui/attr-named-params-on-server.stderr b/derive/tests/ui/attr-named-params-on-server.stderr new file mode 100644 index 000000000..41ccc852a --- /dev/null +++ b/derive/tests/ui/attr-named-params-on-server.stderr @@ -0,0 +1,9 @@ +error: `params = "named"` can only be used to generate a client (on a trait annotated with #[rpc(client)]). At this time the server does not support named parameters. + --> $DIR/attr-named-params-on-server.rs:4:1 + | +4 | / pub trait Rpc { +5 | | /// Returns a protocol version +6 | | #[rpc(name = "add", params = "named")] +7 | | fn add(&self, a: u32, b: u32) -> Result; +8 | | } + | |_^ diff --git a/derive/tests/ui/trait-attr-named-params-on-server.rs b/derive/tests/ui/trait-attr-named-params-on-server.rs new file mode 100644 index 000000000..302768fcf --- /dev/null +++ b/derive/tests/ui/trait-attr-named-params-on-server.rs @@ -0,0 +1,7 @@ +use jsonrpc_derive::rpc; + +#[rpc(server, params = "named")] +pub trait Rpc { +} + +fn main() {} diff --git a/derive/tests/ui/trait-attr-named-params-on-server.stderr b/derive/tests/ui/trait-attr-named-params-on-server.stderr new file mode 100644 index 000000000..c44d44465 --- /dev/null +++ b/derive/tests/ui/trait-attr-named-params-on-server.stderr @@ -0,0 +1,7 @@ +error: custom attribute panicked + --> $DIR/trait-attr-named-params-on-server.rs:3:1 + | +3 | #[rpc(server, params = "named")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: message: Server code generation only supports `params = "positional"` (default) or `params = "raw" at this time.