jsonrpsee_proc_macros/
rpc_macro.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.
26
27//! Declaration of the JSON RPC generator procedural macros.
28
29use std::borrow::Cow;
30
31use crate::attributes::{
32	optional, parse_param_kind, Aliases, Argument, AttributeMeta, MissingArgument, NameMapping, ParamKind,
33};
34use crate::helpers::extract_doc_comments;
35use proc_macro2::TokenStream as TokenStream2;
36use quote::quote;
37use syn::spanned::Spanned;
38use syn::{punctuated::Punctuated, Attribute, Token};
39
40/// Represents a single argument in a RPC call.
41///
42/// stores modifications based on attributes
43#[derive(Debug, Clone)]
44pub struct RpcFnArg {
45	pub(crate) arg_pat: syn::PatIdent,
46	rename_to: Option<String>,
47	pub(crate) ty: syn::Type,
48}
49
50impl RpcFnArg {
51	pub fn from_arg_attrs(arg_pat: syn::PatIdent, ty: syn::Type, attrs: &mut Vec<syn::Attribute>) -> syn::Result<Self> {
52		let mut rename_to = None;
53
54		if let Some(attr) = find_attr(attrs, "argument") {
55			let [rename] = AttributeMeta::parse(attr.clone())?.retain(["rename"])?;
56
57			let rename = optional(rename, Argument::string)?;
58
59			if let Some(rename) = rename {
60				rename_to = Some(rename);
61			}
62		}
63
64		// remove argument attribute after inspection
65		attrs.retain(|attr| !attr.meta.path().is_ident("argument"));
66
67		Ok(Self { arg_pat, rename_to, ty })
68	}
69
70	/// Return the pattern identifier of the argument.
71	pub fn arg_pat(&self) -> &syn::PatIdent {
72		&self.arg_pat
73	}
74	/// Return the string representation of this argument when (de)seriaizing.
75	pub fn name(&self) -> String {
76		self.rename_to.clone().unwrap_or_else(|| self.arg_pat.ident.to_string())
77	}
78	/// Return the type of the argument.
79	pub fn ty(&self) -> &syn::Type {
80		&self.ty
81	}
82}
83
84#[derive(Debug, Clone)]
85pub struct RpcMethod {
86	pub name: String,
87	pub blocking: bool,
88	pub docs: TokenStream2,
89	pub deprecated: TokenStream2,
90	pub params: Vec<RpcFnArg>,
91	pub param_kind: ParamKind,
92	pub returns: Option<syn::Type>,
93	pub signature: syn::TraitItemFn,
94	pub aliases: Vec<String>,
95	pub with_extensions: bool,
96}
97
98impl RpcMethod {
99	pub fn from_item(attr: Attribute, mut method: syn::TraitItemFn) -> syn::Result<Self> {
100		let [aliases, blocking, name, param_kind, with_extensions] =
101			AttributeMeta::parse(attr)?.retain(["aliases", "blocking", "name", "param_kind", "with_extensions"])?;
102
103		let aliases = parse_aliases(aliases)?;
104		let blocking = optional(blocking, Argument::flag)?.is_some();
105		let name = name?.string()?;
106		let param_kind = parse_param_kind(param_kind)?;
107		let with_extensions = optional(with_extensions, Argument::flag)?.is_some();
108
109		let docs = extract_doc_comments(&method.attrs);
110		let deprecated = match find_attr(&method.attrs, "deprecated") {
111			Some(attr) => quote!(#attr),
112			None => quote!(),
113		};
114
115		if blocking && method.sig.asyncness.is_some() {
116			return Err(syn::Error::new(method.sig.span(), "Blocking method must be synchronous"));
117		}
118
119		let params: Vec<_> = method
120			.sig
121			.inputs
122			.iter_mut()
123			.filter_map(|arg| match arg {
124				syn::FnArg::Receiver(_) => None,
125				syn::FnArg::Typed(arg) => match &*arg.pat {
126					syn::Pat::Ident(name) => {
127						Some(RpcFnArg::from_arg_attrs(name.clone(), (*arg.ty).clone(), &mut arg.attrs))
128					}
129					syn::Pat::Wild(wild) => Some(Err(syn::Error::new(
130						wild.underscore_token.span(),
131						"Method argument names must be valid Rust identifiers; got `_` instead",
132					))),
133					_ => Some(Err(syn::Error::new(
134						arg.span(),
135						format!("Unexpected method signature input; got {:?} ", *arg.pat),
136					))),
137				},
138			})
139			.collect::<Result<_, _>>()?;
140
141		let returns = match method.sig.output.clone() {
142			syn::ReturnType::Default => None,
143			syn::ReturnType::Type(_, output) => Some(*output),
144		};
145
146		// We've analyzed attributes and don't need them anymore.
147		method.attrs.clear();
148
149		Ok(Self {
150			aliases,
151			blocking,
152			name,
153			params,
154			param_kind,
155			returns,
156			signature: method,
157			docs,
158			deprecated,
159			with_extensions,
160		})
161	}
162}
163
164#[derive(Debug, Clone)]
165pub struct RpcSubscription {
166	pub name: String,
167	/// When subscribing to an RPC, users can override the content of the `method` field
168	/// in the JSON data sent to subscribers.
169	/// Each subscription thus has one method name to set up the subscription,
170	/// one to unsubscribe and, optionally, a third method name used to describe the
171	/// payload (aka "notification") sent back from the server to subscribers.
172	/// If no override is provided, the subscription method name is used.
173	pub notif_name_override: Option<String>,
174	pub docs: TokenStream2,
175	pub unsubscribe: String,
176	pub params: Vec<RpcFnArg>,
177	pub param_kind: ParamKind,
178	pub item: syn::Type,
179	pub signature: syn::TraitItemFn,
180	pub aliases: Vec<String>,
181	pub unsubscribe_aliases: Vec<String>,
182	pub with_extensions: bool,
183}
184
185impl RpcSubscription {
186	pub fn from_item(attr: syn::Attribute, mut sub: syn::TraitItemFn) -> syn::Result<Self> {
187		let [aliases, item, name, param_kind, unsubscribe, unsubscribe_aliases, with_extensions] =
188			AttributeMeta::parse(attr)?.retain([
189				"aliases",
190				"item",
191				"name",
192				"param_kind",
193				"unsubscribe",
194				"unsubscribe_aliases",
195				"with_extensions",
196			])?;
197
198		let aliases = parse_aliases(aliases)?;
199		let map = name?.value::<NameMapping>()?;
200		let name = map.name;
201		let notif_name_override = map.mapped;
202		let item = item?.value()?;
203		let param_kind = parse_param_kind(param_kind)?;
204		let unsubscribe_aliases = parse_aliases(unsubscribe_aliases)?;
205		let with_extensions = optional(with_extensions, Argument::flag)?.is_some();
206
207		let docs = extract_doc_comments(&sub.attrs);
208		let unsubscribe = match parse_subscribe(unsubscribe)? {
209			Some(unsub) => unsub,
210			None => build_unsubscribe_method(&name).unwrap_or_else(||
211				panic!("Could not generate the unsubscribe method with name '{name}'. You need to provide the name manually using the `unsubscribe` attribute in your RPC API definition"),
212			),
213		};
214
215		let params: Vec<_> = sub
216			.sig
217			.inputs
218			.iter_mut()
219			.filter_map(|arg| match arg {
220				syn::FnArg::Receiver(_) => None,
221				syn::FnArg::Typed(arg) => match &*arg.pat {
222					syn::Pat::Ident(name) => {
223						Some(RpcFnArg::from_arg_attrs(name.clone(), (*arg.ty).clone(), &mut arg.attrs))
224					}
225					_ => panic!("Identifier in signature must be an ident"),
226				},
227			})
228			.collect::<Result<_, _>>()?;
229
230		// We've analyzed attributes and don't need them anymore.
231		sub.attrs.clear();
232
233		Ok(Self {
234			name,
235			notif_name_override,
236			unsubscribe,
237			unsubscribe_aliases,
238			params,
239			param_kind,
240			item,
241			signature: sub,
242			aliases,
243			docs,
244			with_extensions,
245		})
246	}
247}
248
249#[derive(Debug)]
250pub struct RpcDescription {
251	/// Path to the `jsonrpsee` client types part.
252	pub(crate) jsonrpsee_client_path: Option<TokenStream2>,
253	/// Path to the `jsonrpsee` server types part.
254	pub(crate) jsonrpsee_server_path: Option<TokenStream2>,
255	/// Switch denoting that server trait must be generated.
256	/// Assuming that trait to which attribute is applied is named `Foo`, the generated
257	/// server trait will have `FooServer` name.
258	pub(crate) needs_server: bool,
259	/// Switch denoting that client extension trait must be generated.
260	/// Assuming that trait to which attribute is applied is named `Foo`, the generated
261	/// client trait will have `FooClient` name.
262	pub(crate) needs_client: bool,
263	/// Optional prefix for RPC namespace.
264	pub(crate) namespace: Option<String>,
265	/// Trait definition in which all the attributes were stripped.
266	pub(crate) trait_def: syn::ItemTrait,
267	/// List of RPC methods defined in the trait.
268	pub(crate) methods: Vec<RpcMethod>,
269	/// List of RPC subscriptions defined in the trait.
270	pub(crate) subscriptions: Vec<RpcSubscription>,
271	/// Optional user defined trait bounds for the client implementation.
272	pub(crate) client_bounds: Option<Punctuated<syn::WherePredicate, Token![,]>>,
273	/// Optional user defined trait bounds for the server implementation.
274	pub(crate) server_bounds: Option<Punctuated<syn::WherePredicate, Token![,]>>,
275}
276
277impl RpcDescription {
278	pub fn from_item(attr: Attribute, mut item: syn::ItemTrait) -> syn::Result<Self> {
279		let [client, server, namespace, client_bounds, server_bounds] =
280			AttributeMeta::parse(attr)?.retain(["client", "server", "namespace", "client_bounds", "server_bounds"])?;
281
282		let needs_server = optional(server, Argument::flag)?.is_some();
283		let needs_client = optional(client, Argument::flag)?.is_some();
284		let namespace = optional(namespace, Argument::string)?;
285		let client_bounds = optional(client_bounds, Argument::group)?;
286		let server_bounds = optional(server_bounds, Argument::group)?;
287		if !needs_server && !needs_client {
288			return Err(syn::Error::new_spanned(&item.ident, "Either 'server' or 'client' attribute must be applied"));
289		}
290
291		if client_bounds.is_some() && !needs_client {
292			return Err(syn::Error::new_spanned(
293				&item.ident,
294				"Attribute 'client' must be specified with 'client_bounds'",
295			));
296		}
297
298		if server_bounds.is_some() && !needs_server {
299			return Err(syn::Error::new_spanned(
300				&item.ident,
301				"Attribute 'server' must be specified with 'server_bounds'",
302			));
303		}
304
305		let jsonrpsee_client_path = crate::helpers::find_jsonrpsee_client_crate().ok();
306		let jsonrpsee_server_path = crate::helpers::find_jsonrpsee_server_crate().ok();
307
308		if needs_client && jsonrpsee_client_path.is_none() {
309			return Err(syn::Error::new_spanned(&item.ident, "Unable to locate 'jsonrpsee' client dependency"));
310		}
311		if needs_server && jsonrpsee_server_path.is_none() {
312			return Err(syn::Error::new_spanned(&item.ident, "Unable to locate 'jsonrpsee' server dependency"));
313		}
314
315		item.attrs.clear(); // Remove RPC attributes.
316
317		let mut methods = Vec::new();
318		let mut subscriptions = Vec::new();
319
320		// Go through all the methods in the trait and collect methods and
321		// subscriptions.
322		for entry in item.items.iter() {
323			if let syn::TraitItem::Fn(method) = entry {
324				if method.sig.receiver().is_none() {
325					return Err(syn::Error::new_spanned(&method.sig, "First argument of the trait must be '&self'"));
326				}
327
328				let mut is_method = false;
329				let mut is_sub = false;
330				if let Some(attr) = find_attr(&method.attrs, "method") {
331					is_method = true;
332
333					let method_data = RpcMethod::from_item(attr.clone(), method.clone())?;
334
335					methods.push(method_data);
336				}
337				if let Some(attr) = find_attr(&method.attrs, "subscription") {
338					is_sub = true;
339					if is_method {
340						return Err(syn::Error::new_spanned(
341							method,
342							"Element cannot be both subscription and method at the same time",
343						));
344					}
345
346					let sub_data = RpcSubscription::from_item(attr.clone(), method.clone())?;
347					subscriptions.push(sub_data);
348				}
349
350				if !is_method && !is_sub {
351					return Err(syn::Error::new_spanned(
352						method,
353						"Methods must have either 'method' or 'subscription' attribute",
354					));
355				}
356			} else {
357				return Err(syn::Error::new_spanned(entry, "Only methods allowed in RPC traits"));
358			}
359		}
360
361		if methods.is_empty() && subscriptions.is_empty() {
362			return Err(syn::Error::new_spanned(&item, "RPC cannot be empty"));
363		}
364
365		Ok(Self {
366			jsonrpsee_client_path,
367			jsonrpsee_server_path,
368			needs_server,
369			needs_client,
370			namespace,
371			trait_def: item,
372			methods,
373			subscriptions,
374			client_bounds,
375			server_bounds,
376		})
377	}
378
379	pub fn render(self) -> Result<TokenStream2, syn::Error> {
380		let server_impl = if self.needs_server { self.render_server()? } else { TokenStream2::new() };
381		let client_impl = if self.needs_client { self.render_client()? } else { TokenStream2::new() };
382
383		Ok(quote! {
384			#server_impl
385			#client_impl
386		})
387	}
388
389	/// Formats the identifier as a path relative to the resolved
390	/// `jsonrpsee` client path.
391	pub(crate) fn jrps_client_item(&self, item: impl quote::ToTokens) -> TokenStream2 {
392		let jsonrpsee = self.jsonrpsee_client_path.as_ref().unwrap();
393		quote! { #jsonrpsee::#item }
394	}
395
396	/// Formats the identifier as a path relative to the resolved
397	/// `jsonrpsee` server path.
398	pub(crate) fn jrps_server_item(&self, item: impl quote::ToTokens) -> TokenStream2 {
399		let jsonrpsee = self.jsonrpsee_server_path.as_ref().unwrap();
400		quote! { #jsonrpsee::#item }
401	}
402
403	/// Based on the namespace, renders the full name of the RPC method/subscription.
404	/// Examples:
405	/// For namespace `foo` and method `makeSpam`, result will be `foo_makeSpam`.
406	/// For no namespace and method `makeSpam` it will be just `makeSpam`.
407	pub(crate) fn rpc_identifier<'a>(&self, method: &'a str) -> Cow<'a, str> {
408		if let Some(ns) = &self.namespace {
409			format!("{ns}_{method}").into()
410		} else {
411			Cow::Borrowed(method)
412		}
413	}
414}
415
416fn parse_aliases(arg: Result<Argument, MissingArgument>) -> syn::Result<Vec<String>> {
417	let aliases = optional(arg, Argument::value::<Aliases>)?;
418
419	Ok(aliases.map(|a| a.list.into_iter().map(|lit| lit.value()).collect()).unwrap_or_default())
420}
421
422fn parse_subscribe(arg: Result<Argument, MissingArgument>) -> syn::Result<Option<String>> {
423	let unsub = optional(arg, Argument::string)?;
424
425	Ok(unsub)
426}
427
428fn find_attr<'a>(attrs: &'a [Attribute], ident: &str) -> Option<&'a Attribute> {
429	attrs.iter().find(|a| a.path().is_ident(ident))
430}
431
432fn build_unsubscribe_method(method: &str) -> Option<String> {
433	method.strip_prefix("subscribe").map(|s| format!("unsubscribe{s}"))
434}