jsonrpsee_proc_macros/
render_client.rs

1// Copyright 2019-2021 Parity Technologies (UK) Ltd.
2//
3// Permission is hereby granted, free of charge, to any
4// person obtaining a copy of this software and associated
5// documentation files (the "Software"), to deal in the
6// Software without restriction, including without
7// limitation the rights to use, copy, modify, merge,
8// publish, distribute, sublicense, and/or sell copies of
9// the Software, and to permit persons to whom the Software
10// is furnished to do so, subject to the following
11// conditions:
12//
13// The above copyright notice and this permission notice
14// shall be included in all copies or substantial portions
15// of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25// DEALINGS IN THE SOFTWARE.
26use crate::attributes::ParamKind;
27use crate::helpers::generate_where_clause;
28use crate::rpc_macro::{RpcDescription, RpcFnArg, RpcMethod, RpcSubscription};
29use proc_macro2::TokenStream as TokenStream2;
30use quote::{quote, quote_spanned};
31use syn::spanned::Spanned;
32use syn::{AngleBracketedGenericArguments, FnArg, Ident, Pat, PatIdent, PatType, PathArguments, TypeParam};
33
34impl RpcDescription {
35	pub(super) fn render_client(&self) -> Result<TokenStream2, syn::Error> {
36		let jsonrpsee = self.jsonrpsee_client_path.as_ref().unwrap();
37		let sub_tys: Vec<syn::Type> = self.subscriptions.clone().into_iter().map(|s| s.item).collect();
38
39		let trait_name = quote::format_ident!("{}Client", &self.trait_def.ident);
40		let where_clause = generate_where_clause(&self.trait_def, &sub_tys, true, self.client_bounds.as_ref());
41		let type_idents = self.trait_def.generics.type_params().collect::<Vec<&TypeParam>>();
42		let (impl_generics, type_generics, _) = self.trait_def.generics.split_for_impl();
43
44		let super_trait = if self.subscriptions.is_empty() {
45			quote! { #jsonrpsee::core::client::ClientT }
46		} else {
47			quote! { #jsonrpsee::core::client::SubscriptionClientT }
48		};
49
50		let method_impls =
51			self.methods.iter().map(|method| self.render_method(method)).collect::<Result<Vec<_>, _>>()?;
52		let sub_impls = self.subscriptions.iter().map(|sub| self.render_sub(sub)).collect::<Result<Vec<_>, _>>()?;
53
54		let async_trait = self.jrps_client_item(quote! { core::__reexports::async_trait });
55
56		// Doc-comment to be associated with the client.
57		let doc_comment = format!("Client implementation for the `{}` RPC API.", &self.trait_def.ident);
58		let trait_impl = quote! {
59			#[#async_trait]
60			#[doc = #doc_comment]
61			pub trait #trait_name #impl_generics: #super_trait where #(#where_clause,)* {
62				#(#method_impls)*
63				#(#sub_impls)*
64			}
65
66			impl<TypeJsonRpseeInteral #(,#type_idents)*> #trait_name #type_generics for TypeJsonRpseeInteral where TypeJsonRpseeInteral: #super_trait #(,#where_clause)* {}
67		};
68
69		Ok(trait_impl)
70	}
71
72	/// Verify and rewrite the return type (for methods).
73	fn return_result_type(&self, mut ty: syn::Type) -> TokenStream2 {
74		// We expect a valid type path.
75		let syn::Type::Path(ref mut type_path) = ty else {
76			return quote_spanned!(ty.span() => compile_error!("Expecting something like 'Result<Foo, Err>' here. (1)"));
77		};
78
79		// The path (eg std::result::Result) should have a final segment like 'Result'.
80		let Some(type_name) = type_path.path.segments.last_mut() else {
81			return quote_spanned!(ty.span() => compile_error!("Expecting this path to end in something like 'Result<Foo, Err>'"));
82		};
83
84		// Get the generic args eg the <T, E> in Result<T, E>.
85		let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = &mut type_name.arguments
86		else {
87			return quote_spanned!(ty.span() => compile_error!("Expecting something like 'Result<Foo, Err>' here, but got no generic args (eg no '<Foo,Err>')."));
88		};
89
90		if type_name.ident == "Result" {
91			// Result<T, E> should have 2 generic args.
92			if args.len() != 2 {
93				return quote_spanned!(args.span() => compile_error!("Result must be have two arguments"));
94			}
95
96			// Force the last argument to be `jsonrpsee::core::ClientError`:
97			let error_arg = args.last_mut().unwrap();
98			*error_arg =
99				syn::GenericArgument::Type(syn::Type::Verbatim(self.jrps_client_item(quote! { core::client::Error })));
100
101			quote!(#ty)
102		} else if type_name.ident == "RpcResult" {
103			// RpcResult<T> (an alias we export) should have 1 generic arg.
104			if args.len() != 1 {
105				return quote_spanned!(args.span() => compile_error!("RpcResult must have one argument"));
106			}
107
108			// The type alias `RpcResult<T>` is modified to `Result<T, Error>`.
109			let ret_ty = args.last_mut().unwrap();
110			let err_ty = self.jrps_client_item(quote! { core::client::Error });
111
112			quote! { core::result::Result<#ret_ty, #err_ty> }
113		} else if type_name.ident == "ResponsePayload" {
114			// ResponsePayload<'a, T>
115			if args.len() != 2 {
116				return quote_spanned!(args.span() => compile_error!("ResponsePayload must have exactly two arguments"));
117			}
118
119			// The type alias `RpcResult<T>` is modified to `Result<T, Error>`.
120			let ret_ty = args.last_mut().unwrap();
121			let err_ty = self.jrps_client_item(quote! { core::client::Error });
122
123			quote! { core::result::Result<#ret_ty, #err_ty> }
124		} else {
125			// Any other type name isn't allowed.
126			quote_spanned!(type_name.span() => compile_error!("The return type must be Result<T, Error>, RpcResult<T> or ResponsePayload<'static, T>"))
127		}
128	}
129
130	fn render_method(&self, method: &RpcMethod) -> Result<TokenStream2, syn::Error> {
131		// `jsonrpsee::Error`
132		let jrps_error = self.jrps_client_item(quote! { core::client::Error });
133		// Rust method to invoke (e.g. `self.<foo>(...)`).
134		let rust_method_name = &method.signature.sig.ident;
135		// List of inputs to put into `Params` (e.g. `self.foo(<12, "baz">)`).
136		// Includes `&self` receiver.
137		let rust_method_params = &method.signature.sig.inputs;
138		// Name of the RPC method (e.g. `foo_makeSpam`).
139		let rpc_method_name = self.rpc_identifier(&method.name);
140
141		// Called method is either `request` or `notification`.
142		// `returns` represent the return type of the *rust method* (`Result<T, jsonrpsee::core::ClientError>`).
143		let (called_method, returns) = if let Some(returns) = &method.returns {
144			let called_method = quote::format_ident!("request");
145			let returns = self.return_result_type(returns.clone());
146			let returns = quote! { #returns };
147
148			(called_method, returns)
149		} else {
150			let called_method = quote::format_ident!("notification");
151			let returns = quote! { Result<(), #jrps_error> };
152
153			(called_method, returns)
154		};
155
156		// Encoded parameters for the request.
157		let parameter_builder = self.encode_params(&method.params, &method.param_kind, &method.signature);
158		// Doc-comment to be associated with the method.
159		let docs = &method.docs;
160		// Mark the method as deprecated, if previously declared as so.
161		let deprecated = &method.deprecated;
162
163		let method = quote! {
164			#docs
165			#deprecated
166			#[allow(non_snake_case)]
167			#[allow(clippy::used_underscore_binding)]
168			async fn #rust_method_name(#rust_method_params) -> #returns {
169				let params = { #parameter_builder };
170				self.#called_method(#rpc_method_name, params).await
171			}
172		};
173		Ok(method)
174	}
175
176	fn render_sub(&self, sub: &RpcSubscription) -> Result<TokenStream2, syn::Error> {
177		// `jsonrpsee::core::ClientError`
178		let jrps_error = self.jrps_client_item(quote! { core::client::Error });
179		// Rust method to invoke (e.g. `self.<foo>(...)`).
180		let rust_method_name = &sub.signature.sig.ident;
181		// List of inputs to put into `Params` (e.g. `self.foo(<12, "baz">)`).
182		let rust_method_params = &sub.signature.sig.inputs;
183		// Name of the RPC subscription (e.g. `foo_sub`).
184		let rpc_sub_name = self.rpc_identifier(&sub.name);
185		// Name of the RPC method to unsubscribe (e.g. `foo_unsub`).
186		let rpc_unsub_name = self.rpc_identifier(&sub.unsubscribe);
187
188		// `returns` represent the return type of the *rust method*, which is wrapped
189		// into the `Subscription` object.
190		let sub_type = self.jrps_client_item(quote! { core::client::Subscription });
191		let item = &sub.item;
192		let returns = quote! { Result<#sub_type<#item>, #jrps_error> };
193
194		// Encoded parameters for the request.
195		let parameter_builder = self.encode_params(&sub.params, &sub.param_kind, &sub.signature);
196		// Doc-comment to be associated with the method.
197		let docs = &sub.docs;
198
199		let method = quote! {
200			#docs
201			#[allow(non_snake_case)]
202			#[allow(clippy::used_underscore_binding)]
203			async fn #rust_method_name(#rust_method_params) -> #returns {
204				let params = #parameter_builder;
205				self.subscribe(#rpc_sub_name, params, #rpc_unsub_name).await
206			}
207		};
208		Ok(method)
209	}
210
211	fn encode_params(&self, params: &[RpcFnArg], param_kind: &ParamKind, signature: &syn::TraitItemFn) -> TokenStream2 {
212		const ILLEGAL_PARAM_NAME: &str = "__RpcParams__";
213
214		let jsonrpsee = self.jsonrpsee_client_path.as_ref().unwrap();
215		let p = Ident::new(ILLEGAL_PARAM_NAME, proc_macro2::Span::call_site());
216
217		let reexports = self.jrps_client_item(quote! { core::__reexports });
218
219		if params.is_empty() {
220			return quote!({
221				#jsonrpsee::core::params::ArrayParams::new()
222			});
223		}
224
225		if params.iter().any(|arg| arg.arg_pat().ident == p) {
226			panic!(
227				"Cannot use `{}` as a parameter name because it's overlapping with an internal variable in the generated code. Change it something else to make it work", ILLEGAL_PARAM_NAME
228			);
229		}
230
231		match param_kind {
232			ParamKind::Map => {
233				// Extract parameter names.
234				let param_names = extract_param_names(&signature.sig);
235				// Combine parameter names and values to pass them as parameters.
236				let params_insert = params.iter().map(|arg| {
237					// Throw away the type.
238					let value = arg.arg_pat();
239					let name = arg.name();
240					quote!(#name, #value)
241				});
242
243				// It's possible that the user has a parameter named `ILLEGAL_PARAM_NAME` in there API
244				// which would conflict with our internal parameter name
245				//
246				// We will throw an error if that is the case.
247				if param_names.iter().any(|name| name == ILLEGAL_PARAM_NAME) {
248					panic!("Cannot use `{}` as a parameter name", ILLEGAL_PARAM_NAME);
249				}
250
251				quote!({
252					let mut #p = #jsonrpsee::core::params::ObjectParams::new();
253					#(
254						if let Err(err) = #p.insert(#params_insert) {
255							#reexports::panic_fail_serialize(stringify!(#params_insert), err);
256						}
257					)*
258					#p
259				})
260			}
261			ParamKind::Array => {
262				// Throw away the type.
263				let params = params.iter().map(RpcFnArg::arg_pat);
264
265				quote!({
266					let mut #p = #jsonrpsee::core::params::ArrayParams::new();
267					#(
268						if let Err(err) = #p.insert(#params) {
269							#reexports::panic_fail_serialize(stringify!(#params), err);
270						}
271					)*
272					#p
273				})
274			}
275		}
276	}
277}
278
279fn extract_param_names(sig: &syn::Signature) -> Vec<String> {
280	sig.inputs
281		.iter()
282		.filter_map(|param| match param {
283			FnArg::Typed(PatType { pat, .. }) => match &**pat {
284				Pat::Ident(PatIdent { ident, .. }) => Some(ident.to_string()),
285				_ => None,
286			},
287			_ => None,
288		})
289		.collect()
290}