Unverified Commit 50d8bf78 authored by Niklas Adolfsson's avatar Niklas Adolfsson Committed by GitHub
Browse files

feat: client trait + porting client proc macros (#199)



* draft: client trait sketch...

* use async_trait instead of `Box<Future>`

* refactor client trait

* [types]: shared client types.

* integrate with `jsonrpsee_proc_macros::rpc_api`

* Update proc-macros/src/lib.rs

* integrate with proc macros

* fix: hacky support for generic types in proc macro

* fix: make the examples work.

* trait: error associated type

* Update types/src/traits.rs

* client trait: make object safe.

Make the trait usable as a trait object i.e, `Box<dyn Trait>`

* client trait: remove `Self::Sized`

* add tests for proc macros.

* fix build

* fmt

* [client trait]: remove unused associated error typ

* [proc macros]: separate enum variant each return_t

* add tracking issue to `TODO`

* doc(client trait): improve documentation.

* separate trait for subscribing client

* add documentation

* proc macros: remove debug impl

* fix compile warns

* address grumbles: remove fn process_response

* Update types/src/client.rs

Co-authored-by: David's avatarDavid <dvdplm@gmail.com>

* Update types/src/client.rs

Co-authored-by: David's avatarDavid <dvdplm@gmail.com>

* Update types/src/client.rs

Co-authored-by: David's avatarDavid <dvdplm@gmail.com>

* Update types/src/client.rs

Co-authored-by: David's avatarDavid <dvdplm@gmail.com>

* Update types/src/client.rs

Co-authored-by: David's avatarDavid <dvdplm@gmail.com>

* remove old proc macro tests

* address grumbles: shorter lines

Co-authored-by: David's avatarDavid <dvdplm@gmail.com>
parent 5db1da00
......@@ -10,4 +10,5 @@ members = [
"utils",
"ws-client",
"ws-server",
"proc-macros",
]
......@@ -3,7 +3,10 @@ use criterion::*;
use futures::channel::oneshot::{self, Sender};
use jsonrpsee_http_client::{HttpClient, HttpConfig};
use jsonrpsee_http_server::HttpServer;
use jsonrpsee_types::jsonrpc::{JsonValue, Params};
use jsonrpsee_types::{
jsonrpc::{JsonValue, Params},
traits::Client,
};
use jsonrpsee_ws_client::{WsClient, WsConfig};
use jsonrpsee_ws_server::WsServer;
use std::net::SocketAddr;
......
......@@ -17,4 +17,5 @@ jsonrpsee-http-client = { path = "../http-client", version = "0.1" }
jsonrpsee-ws-client = { path = "../ws-client", version = "0.1" }
jsonrpsee-ws-server = { path = "../ws-server", version = "0.1" }
jsonrpsee-http-server = { path = "../http-server", version = "0.1" }
jsonrpsee-proc-macros = { path = "../proc-macros", version = "0.2" }
tokio = { version = "1", features = ["full"] }
......@@ -28,7 +28,10 @@ use async_std::task;
use futures::channel::oneshot::{self, Sender};
use jsonrpsee_http_client::{HttpClient, HttpConfig};
use jsonrpsee_http_server::HttpServer;
use jsonrpsee_types::jsonrpc::{JsonValue, Params};
use jsonrpsee_types::{
jsonrpc::{JsonValue, Params},
traits::Client,
};
const SOCK_ADDR: &str = "127.0.0.1:9933";
const SERVER_URI: &str = "http://localhost:9933";
......
......@@ -25,7 +25,10 @@
// DEALINGS IN THE SOFTWARE.
use futures::channel::oneshot::{self, Sender};
use jsonrpsee_types::jsonrpc::{JsonValue, Params};
use jsonrpsee_types::{
jsonrpc::{JsonValue, Params},
traits::Client,
};
use jsonrpsee_ws_client::{WsClient, WsConfig};
use jsonrpsee_ws_server::WsServer;
use tokio::task;
......@@ -43,8 +46,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
});
server_started_rx.await?;
let config = WsConfig::with_url(SERVER_URI);
let client = WsClient::new(config).await?;
let client = WsClient::new(WsConfig::with_url(SERVER_URI)).await?;
let response: JsonValue = client.request("say_hello", Params::None).await?;
println!("r: {:?}", response);
......
......@@ -25,7 +25,10 @@
// DEALINGS IN THE SOFTWARE.
use futures::channel::oneshot::{self, Sender};
use jsonrpsee_types::jsonrpc::{JsonValue, Params};
use jsonrpsee_types::{
jsonrpc::{JsonValue, Params},
traits::SubscriptionClient,
};
use jsonrpsee_ws_client::{WsClient, WsConfig, WsSubscription};
use jsonrpsee_ws_server::WsServer;
use tokio::task;
......
......@@ -7,6 +7,7 @@ edition = "2018"
license = "MIT"
[dependencies]
async-trait = "0.1"
futures = "0.3"
hyper14 = { package = "hyper", version = "0.14", features = ["client", "http1", "http2", "tcp"], optional = true }
hyper13 = { package = "hyper", version = "0.13", optional = true }
......
use crate::transport::HttpTransportClient;
use async_trait::async_trait;
use jsonrpc::DeserializeOwned;
use jsonrpsee_types::{
error::Error,
http::HttpConfig,
jsonrpc::{self, JsonValue},
};
use jsonrpsee_types::{error::Error, http::HttpConfig, jsonrpc, traits::Client};
use std::convert::TryInto;
use std::sync::atomic::{AtomicU64, Ordering};
......@@ -27,15 +24,15 @@ impl HttpClient {
let transport = HttpTransportClient::new(target, config).map_err(|e| Error::TransportError(Box::new(e)))?;
Ok(Self { transport, request_id: AtomicU64::new(0) })
}
}
/// Send a notification to the server.
///
/// WARNING: This method must be executed on [Tokio 1.0](https://docs.rs/tokio/1.0.1/tokio).
pub async fn notification(
&self,
method: impl Into<String>,
params: impl Into<jsonrpc::Params>,
) -> Result<(), Error> {
#[async_trait]
impl Client for HttpClient {
async fn notification<M, P>(&self, method: M, params: P) -> Result<(), Error>
where
M: Into<String> + Send,
P: Into<jsonrpc::Params> + Send,
{
let request = jsonrpc::Request::Single(jsonrpc::Call::Notification(jsonrpc::Notification {
jsonrpc: jsonrpc::Version::V2,
method: method.into(),
......@@ -46,15 +43,11 @@ impl HttpClient {
}
/// Perform a request towards the server.
///
/// WARNING: This method must be executed on [Tokio 1.0](https://docs.rs/tokio/1.0.1/tokio).
pub async fn request<Ret>(
&self,
method: impl Into<String>,
params: impl Into<jsonrpc::Params>,
) -> Result<Ret, Error>
async fn request<T, M, P>(&self, method: M, params: P) -> Result<T, Error>
where
Ret: DeserializeOwned,
T: DeserializeOwned,
M: Into<String> + Send,
P: Into<jsonrpc::Params> + Send,
{
// NOTE: `fetch_add` wraps on overflow which is intended.
let id = self.request_id.fetch_add(1, Ordering::SeqCst);
......@@ -72,7 +65,10 @@ impl HttpClient {
.map_err(|e| Error::TransportError(Box::new(e)))?;
let json_value = match response {
jsonrpc::Response::Single(rp) => Self::process_response(rp, id),
jsonrpc::Response::Single(response) => match response.id() {
jsonrpc::Id::Num(n) if n == &id => response.try_into().map_err(Error::Request),
_ => Err(Error::InvalidRequestId),
},
// Server should not send batch response to a single request.
jsonrpc::Response::Batch(_rps) => {
Err(Error::Custom("Server replied with batch response to a single request".to_string()))
......@@ -84,11 +80,4 @@ impl HttpClient {
}?;
jsonrpc::from_value(json_value).map_err(Error::ParseError)
}
fn process_response(response: jsonrpc::Output, expected_id: u64) -> Result<JsonValue, Error> {
match response.id() {
jsonrpc::Id::Num(n) if n == &expected_id => response.try_into().map_err(Error::Request),
_ => Err(Error::InvalidRequestId),
}
}
}
......@@ -3,6 +3,7 @@ use jsonrpsee_types::{
error::Error,
http::HttpConfig,
jsonrpc::{self, ErrorCode, JsonValue, Params},
traits::Client,
};
use jsonrpsee_test_utils::helpers::*;
......
[package]
name = "jsonrpsee-proc-macros"
description = "JSON-RPC crate"
version = "1.0.0"
authors = ["Pierre Krieger <pierre.krieger1708@gmail.com>"]
version = "0.2.0"
authors = ["Parity Technologies <admin@parity.io>", "Pierre Krieger <pierre.krieger1708@gmail.com>"]
license = "MIT"
edition = "2018"
......@@ -13,4 +13,4 @@ proc-macro = true
Inflector = "0.11.4"
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0", features = ["full", "extra-traits", "parsing", "printing", "proc-macro", "visit"] }
syn = { version = "1.0", features = ["full", "extra-traits", "parsing", "printing", "proc-macro", "visit"] }
\ No newline at end of file
......@@ -28,7 +28,7 @@ extern crate proc_macro;
use inflector::Inflector as _;
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use quote::{format_ident, quote, quote_spanned};
use std::collections::HashSet;
use syn::spanned::Spanned as _;
......@@ -39,7 +39,7 @@ mod api_def;
/// The format within this macro must be:
///
/// ```ignore
/// rpc_api!{
/// jsonrpsee_proc_macros::rpc_client_api! {
/// Foo { ... }
/// pub(crate) Bar { ... }
/// }
......@@ -55,20 +55,41 @@ mod api_def;
/// an enum where each variant corresponds to a function of the definition. Function names are
/// turned into PascalCase to conform to the Rust style guide.
///
/// Each generated enum has a `next_request` method whose signature is:
///
/// ```ignore
/// async fn next_request(server: &'a mut jsonrpsee::raw::RawServer<R, I>) -> Result<Foo<'a, R, I>, std::io::Error>;
/// ```
///
/// This method lets you grab the next request incoming from a server, and parse it to match of
/// the function definitions. Invalid requests are automatically handled.
///
/// Additionally, each generated enum has one method per function definition that lets you perform
/// the method has a client.
///
// TODO(niklasad1): Generic type params for individual methods doesn't work
// because how the enum is generated, so for now type params must be declared on the entire enum.
// The reason is that all type params on the enum is bound as a separate variant but
// not generic params i.e, either params or return type.
// To handle that properly, all generic types has to be collected and applied to the enum, see example:
//
// ```rust
// jsonrpsee_rpc_client_api! {
// Api {
// // Doesn't work.
// fn generic_notif<T>(t: T);
// }
// ```
//
// Expands to which doesn't compile:
// ```rust
// enum Api {
// GenericNotif {
// t: T,
// },
// }
// ```
// The code should be expanded to (to compile):
// ```rust
// enum Api<T> {
// GenericNotif {
// t: T,
// },
// }
// ```
#[proc_macro]
pub fn rpc_api(input_token_stream: TokenStream) -> TokenStream {
pub fn rpc_client_api(input_token_stream: TokenStream) -> TokenStream {
// Start by parsing the input into what we expect.
let defs: api_def::ApiDefinitions = match syn::parse(input_token_stream) {
Ok(d) => d,
......@@ -77,7 +98,7 @@ pub fn rpc_api(input_token_stream: TokenStream) -> TokenStream {
let mut out = Vec::with_capacity(defs.apis.len());
for api in defs.apis {
match build_api(api) {
match build_client_api(api) {
Ok(a) => out.push(a),
Err(err) => return err.to_compile_error().into(),
};
......@@ -89,37 +110,21 @@ pub fn rpc_api(input_token_stream: TokenStream) -> TokenStream {
}
/// Generates the macro output token stream corresponding to a single API.
fn build_api(api: api_def::ApiDefinition) -> Result<proc_macro2::TokenStream, syn::Error> {
fn build_client_api(api: api_def::ApiDefinition) -> Result<proc_macro2::TokenStream, syn::Error> {
let enum_name = &api.name;
// TODO: make sure there's no conflict here
let mut tweaked_generics = api.generics.clone();
tweaked_generics
.params
.insert(0, From::from(syn::LifetimeDef::new(syn::parse_str::<syn::Lifetime>("'a").unwrap())));
tweaked_generics.params.push(From::from(syn::TypeParam::from(syn::parse_str::<syn::Ident>("R").unwrap())));
tweaked_generics.params.push(From::from(syn::TypeParam::from(syn::parse_str::<syn::Ident>("I").unwrap())));
let (impl_generics, ty_generics, where_clause) = tweaked_generics.split_for_impl();
let generics = api
.generics
.params
.iter()
.filter_map(|gp| if let syn::GenericParam::Type(tp) = gp { Some(tp.ident.clone()) } else { None })
.collect::<HashSet<_>>();
let visibility = &api.visibility;
let generics = api.generics.clone();
let mut non_used_type_params = HashSet::new();
let mut variants = Vec::new();
let mut tmp_variants = Vec::new();
for function in &api.definitions {
let function_is_notification = function.is_void_ret_type();
let variant_name = snake_case_to_camel_case(&function.signature.ident);
let ret = match &function.signature.output {
syn::ReturnType::Default => quote! {()},
syn::ReturnType::Type(_, ty) => quote_spanned!(ty.span()=> #ty),
if let syn::ReturnType::Type(_, ty) = &function.signature.output {
non_used_type_params.insert(ty);
};
let mut params_list = Vec::new();
for input in function.signature.inputs.iter() {
let (ty, pat_span, param_variant_name) = match input {
syn::FnArg::Receiver(_) => {
......@@ -130,214 +135,35 @@ fn build_api(api: api_def::ApiDefinition) -> Result<proc_macro2::TokenStream, sy
}
syn::FnArg::Typed(syn::PatType { ty, pat, .. }) => (ty, pat.span(), param_variant_name(&pat)?),
};
params_list.push(quote_spanned!(pat_span=> #param_variant_name: #ty));
}
if !function_is_notification {
if params_list.is_empty() {
tmp_variants.push(quote_spanned!(function.signature.ident.span()=> #variant_name));
} else {
tmp_variants.push(quote_spanned!(function.signature.ident.span()=>
#variant_name {
#(#params_list,)*
}
));
variants.push(quote_spanned!(function.signature.ident.span()=>
#variant_name {
#(#params_list,)*
}
}
if function_is_notification {
variants.push(quote_spanned!(function.signature.ident.span()=>
#variant_name {
#(#params_list,)*
}
));
} else {
variants.push(quote_spanned!(function.signature.ident.span()=>
#variant_name {
respond: jsonrpsee::raw::server::TypedResponder<'a, R, I, #ret>,
#(#params_list,)*
}
));
}
));
}
let next_request = {
let mut notifications_blocks = Vec::new();
let mut function_blocks = Vec::new();
let mut tmp_to_rq = Vec::new();
struct GenericParams {
generics: HashSet<syn::Ident>,
types: HashSet<syn::Ident>,
}
impl<'ast> syn::visit::Visit<'ast> for GenericParams {
fn visit_ident(&mut self, ident: &'ast syn::Ident) {
if self.generics.contains(ident) {
self.types.insert(ident.clone());
}
}
}
let mut generic_params = GenericParams { generics, types: HashSet::new() };
for function in &api.definitions {
let function_is_notification = function.is_void_ret_type();
let variant_name = snake_case_to_camel_case(&function.signature.ident);
let rpc_method_name =
function.attributes.method.clone().unwrap_or_else(|| function.signature.ident.to_string());
let mut params_builders = Vec::new();
let mut params_names_list = Vec::new();
for input in function.signature.inputs.iter() {
let (ty, param_variant_name, rpc_param_name) = match input {
syn::FnArg::Receiver(_) => {
return Err(syn::Error::new(
input.span(),
"Having `self` is not allowed in RPC queries definitions",
));
}
syn::FnArg::Typed(syn::PatType { ty, pat, attrs, .. }) => {
(ty, param_variant_name(&pat)?, rpc_param_name(&pat, &attrs)?)
}
};
syn::visit::visit_type(&mut generic_params, &ty);
params_names_list.push(quote_spanned!(function.signature.span()=> #param_variant_name));
if !function_is_notification {
params_builders.push(quote_spanned!(function.signature.span()=>
let #param_variant_name: #ty = {
match request.params().get(#rpc_param_name) {
Ok(v) => v,
Err(_) => {
// TODO: message
request.respond(Err(jsonrpsee::common::Error::invalid_params(#rpc_param_name)));
continue;
}
}
};
));
} else {
params_builders.push(quote_spanned!(function.signature.span()=>
let #param_variant_name: #ty = {
match request.params().get(#rpc_param_name) {
Ok(v) => v,
Err(_) => {
// TODO: log this?
continue;
}
}
};
));
}
}
if function_is_notification {
notifications_blocks.push(quote_spanned!(function.signature.span()=>
if method == #rpc_method_name {
let request = n;
#(#params_builders)*
return Ok(#enum_name::#variant_name { #(#params_names_list),* });
}
));
} else {
function_blocks.push(quote_spanned!(function.signature.span()=>
if request_outcome.is_none() && method == #rpc_method_name {
let request = server.request_by_id(&request_id).unwrap();
#(#params_builders)*
request_outcome = Some(Tmp::#variant_name { #(#params_names_list),* });
}
));
tmp_to_rq.push(quote_spanned!(function.signature.span()=>
Some(Tmp::#variant_name { #(#params_names_list),* }) => {
let request = server.request_by_id(&request_id).unwrap();
let respond = jsonrpsee::raw::server::TypedResponder::from(request);
return Ok(#enum_name::#variant_name { respond #(, #params_names_list)* });
},
));
}
}
let params_tys = generic_params.types.iter();
let tmp_generics = if generic_params.types.is_empty() {
quote!()
} else {
quote_spanned!(api.name.span()=>
<#(#params_tys,)*>
)
};
let on_request = quote_spanned!(api.name.span()=> {
#[allow(unused)] // The enum might be empty
enum Tmp #tmp_generics {
#(#tmp_variants,)*
}
let request_id = r.id();
let method = r.method().to_owned();
let mut request_outcome: Option<Tmp #tmp_generics> = None;
#(#function_blocks)*
match request_outcome {
#(#tmp_to_rq)*
None => server.request_by_id(&request_id).unwrap().respond(Err(jsonrpsee::common::Error::method_not_found())),
}
});
let on_notification = quote_spanned!(api.name.span()=> {
let method = n.method().to_owned();
#(#notifications_blocks)*
// TODO: we received an unknown notification; log this?
});
let params_tys = generic_params.types.iter();
quote_spanned!(api.name.span()=>
#visibility async fn next_request(server: &'a mut jsonrpsee::raw::RawServer<R, I>) -> core::result::Result<#enum_name #ty_generics, std::io::Error>
where
R: jsonrpsee::transport::TransportServer<RequestId = I>,
I: Clone + PartialEq + Eq + std::hash::Hash + Send + Sync
#(, #params_tys: jsonrpsee::common::DeserializeOwned)*
{
loop {
match server.next_event().await {
jsonrpsee::raw::RawServerEvent::Notification(n) => #on_notification,
jsonrpsee::raw::RawServerEvent::SubscriptionsClosed(_) => unimplemented!(), // TODO:
jsonrpsee::raw::RawServerEvent::SubscriptionsReady(_) => unimplemented!(), // TODO:
jsonrpsee::raw::RawServerEvent::Request(r) => #on_request,
}
}
}
)
};
let client_impl_block = build_client_impl(&api)?;
let debug_variants = build_debug_variants(&api)?;
Ok(quote_spanned!(api.name.span()=>
#visibility enum #enum_name #tweaked_generics {
#(#variants),*
}
let mut ret_variants = Vec::new();
for (idx, ty) in non_used_type_params.into_iter().enumerate() {
// NOTE(niklasad1): variant names are converted from `snake_case` to `CamelCase`
// It's impossible to have collisions between `_0, _1, ... _N`
// Because variant name `_0`, `__0` becomes `0` in `CamelCase`
// then `0` is not a valid identifier in Rust syntax and the error message is hard to understand.
// Perhaps document this in macro when it's ready.
let varname = format_ident!("_{}", idx);
ret_variants.push(quote_spanned!(ty.span()=> #varname (#ty)));
}
impl #impl_generics #enum_name #ty_generics #where_clause {
#next_request
Ok(quote_spanned!(api.name.span()=>
#visibility enum #enum_name #generics {
#(#variants,)* #(#[allow(unused)] #ret_variants,)*
}
#client_impl_block
impl #impl_generics std::fmt::Debug for #enum_name #ty_generics #where_clause {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
#(#debug_variants,)*
}
}
}
))
}
......@@ -347,18 +173,11 @@ fn build_api(api: api_def::ApiDefinition) -> Result<proc_macro2::TokenStream, sy
fn build_client_impl(api: &api_def::ApiDefinition) -> Result<proc_macro2::TokenStream, syn::Error> {
let enum_name = &api.name;
let (impl_generics_org, _, where_clause_org) = api.generics.split_for_impl();
let lifetimes_org = api.generics.lifetimes();
let type_params_org = api.generics.type_params();
let const_params_org = api.generics.const_params();
let (impl_generics_org, type_generics, where_clause_org) = api.generics.split_for_impl();
let client_functions = build_client_functions(&api)?;
Ok(quote_spanned!(api.name.span()=>
// TODO: order between type_params and const_params is undecided
impl #impl_generics_org #enum_name<'static #(, #lifetimes_org)* #(, #type_params_org)* #(, #const_params_org)*, (), ()>
#where_clause_org
{
Ok(quote_spanned!(api.name.span() =>
impl #impl_generics_org #enum_name #type_generics #where_clause_org {
#(#client_functions)*
}
))
......@@ -406,27 +225,27 @@ fn build_client_functions(api: &api_def::ApiDefinition) -> Result<Vec<proc_macro
params_to_json.push(quote_spanned!(pat_span=>
map.insert(
#rpc_param_name.to_string(),
jsonrpsee::common::to_value(#generated_param_name.into()).unwrap() // TODO: don't unwrap
jsonrpsee_types::jsonrpc::to_value(#generated_param_name.into()).map_err(|e| jsonrpsee_types::error::Error::Custom(format!("{:?}", e)))?
);
));
params_to_array.push(quote_spanned!(pat_span=>
jsonrpsee::common::to_value(#generated_param_name.into()).unwrap() // TODO: don't unwrap
params_to_array.push(quote_spanned!(pat_span =>
jsonrpsee_types::jsonrpc::to_value(#generated_param_name.into()).map_err(|e| jsonrpsee_types::error::Error::Custom(format!("{:?}", e)))?
));
}
let params_building = if params_list.is_empty() {
quote! {jsonrpsee::common::Params::None}
quote! {jsonrpsee_types::jsonrpc::Params::None}
} else if function.attributes.positional_params {
quote_spanned!(function.signature.span()=>
jsonrpsee::common::Params::Array(vec![
jsonrpsee_types::jsonrpc::Params::Array(vec![
#(#params_to_array),*
])
)
} else {
let params_list_len = params_list.len();
quote_spanned!(function.signature.span()=>
jsonrpsee::common::Params::Map({
let mut map = jsonrpsee::common::JsonMap::with_capacity(#params_list_len);
jsonrpsee_types::jsonrpc::Params::Map({
let mut map = jsonrpsee_types::jsonrpc::JsonMap::with_capacity(#params_list_len);
#(#params_to_json)*
map
})
......@@ -436,48 +255,26 @@ fn build_client_functions(api: &api_def::ApiDefinition) -> Result<Vec<proc_macro