jsonrpsee_proc_macros/
helpers.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
27use std::collections::HashSet;
28
29use crate::visitor::{FindAllParams, FindSubscriptionParams};
30use proc_macro2::{Span, TokenStream as TokenStream2};
31use proc_macro_crate::{crate_name, FoundCrate};
32use quote::quote;
33use syn::{parse_quote, punctuated::Punctuated, token::Comma, visit::Visit, Token, WherePredicate};
34
35/// Search for client-side `jsonrpsee` in `Cargo.toml`.
36pub(crate) fn find_jsonrpsee_client_crate() -> Result<proc_macro2::TokenStream, syn::Error> {
37	find_jsonrpsee_crate(&["jsonrpsee-http-client", "jsonrpsee-ws-client", "jsonrpsee-wasm-client"])
38}
39
40/// Search for server-side `jsonrpsee` in `Cargo.toml`.
41pub(crate) fn find_jsonrpsee_server_crate() -> Result<proc_macro2::TokenStream, syn::Error> {
42	find_jsonrpsee_crate(&["jsonrpsee-server"])
43}
44
45fn find_jsonrpsee_crate(crate_names: &[&str]) -> Result<proc_macro2::TokenStream, syn::Error> {
46	match crate_name("jsonrpsee") {
47		Ok(FoundCrate::Name(name)) => {
48			let ident = syn::Ident::new(&name, Span::call_site());
49			Ok(quote!(#ident))
50		}
51		Ok(FoundCrate::Itself) => panic!("Deriving RPC methods in any of the `jsonrpsee` crates is not supported"),
52		Err(_) => {
53			let mut err = None;
54			for name in crate_names {
55				match crate_name(name) {
56					Ok(FoundCrate::Name(name)) => {
57						let ident = syn::Ident::new(&name, Span::call_site());
58						return Ok(quote!(#ident));
59					}
60					Ok(FoundCrate::Itself) => {
61						err = Some(Err(syn::Error::new(
62							Span::call_site(),
63							"Deriving rpc methods in any of the `jsonrpsee` crates is not supported",
64						)));
65					}
66					Err(e) => {
67						err = Some(Err(syn::Error::new(Span::call_site(), &e)));
68					}
69				}
70			}
71
72			err.expect("Crate names must not be empty; this is a bug please open an issue")
73		}
74	}
75}
76
77/// Traverses the RPC trait definition and applies the required bounds for the generic type parameters that are used.
78/// The bounds applied depend on whether the type parameter is used as a parameter, return value or subscription result
79/// and whether it's used in client or server mode.
80/// Type params get `Send + Sync + 'static` bounds and input/output parameters get `Serialize` and/or `DeserializeOwned`
81/// bounds. Inspired by <https://github.com/paritytech/jsonrpc/blob/master/derive/src/to_delegate.rs#L414>
82///
83/// ### Example
84///
85/// ```
86///  use jsonrpsee::proc_macros::rpc;
87///  use jsonrpsee::core::{RpcResult, SubscriptionResult};
88///
89///  #[rpc(client, server)]
90///  pub trait RpcTrait<A, B, C> {
91///    #[method(name = "call")]
92///    fn call(&self, a: A) -> RpcResult<B>;
93///
94///    #[subscription(name = "subscribe", item = Vec<C>)]
95///    async fn sub(&self) -> SubscriptionResult;
96///  }
97/// ```
98///
99/// Because the `item` attribute is not parsed as ordinary rust syntax, the `syn::Type` is traversed to find
100/// each generic parameter of it.
101/// This is used as an additional input before traversing the entire trait.
102/// Otherwise, it's not possible to know whether a type parameter is used for subscription result.
103pub(crate) fn generate_where_clause(
104	item_trait: &syn::ItemTrait,
105	sub_tys: &[syn::Type],
106	is_client: bool,
107	bounds: Option<&Punctuated<WherePredicate, Comma>>,
108) -> Vec<syn::WherePredicate> {
109	let visitor = visit_trait(item_trait, sub_tys);
110	let additional_where_clause = item_trait.generics.where_clause.clone();
111
112	if let Some(custom_bounds) = bounds {
113		let mut bounds: Vec<_> = additional_where_clause
114			.map(|where_clause| where_clause.predicates.into_iter().collect())
115			.unwrap_or_default();
116
117		bounds.extend(custom_bounds.iter().cloned());
118
119		return bounds;
120	}
121
122	item_trait
123		.generics
124		.type_params()
125		.map(|ty| {
126			let ty_path = syn::TypePath { qself: None, path: ty.ident.clone().into() };
127			let mut bounds: Punctuated<syn::TypeParamBound, Token![+]> = parse_quote!(Send + Sync + 'static);
128
129			if is_client {
130				if visitor.input_params.contains(&ty.ident) {
131					bounds.push(parse_quote!(jsonrpsee::core::Serialize))
132				}
133				if visitor.ret_params.contains(&ty.ident) || visitor.sub_params.contains(&ty.ident) {
134					bounds.push(parse_quote!(jsonrpsee::core::DeserializeOwned))
135				}
136			} else {
137				if visitor.input_params.contains(&ty.ident) {
138					bounds.push(parse_quote!(jsonrpsee::core::DeserializeOwned))
139				}
140				if visitor.ret_params.contains(&ty.ident) {
141					bounds.push(parse_quote!(std::clone::Clone))
142				}
143				if visitor.ret_params.contains(&ty.ident) || visitor.sub_params.contains(&ty.ident) {
144					bounds.push(parse_quote!(jsonrpsee::core::Serialize))
145				}
146			}
147
148			// Add the trait bounds specified in the trait.
149			if let Some(where_clause) = &additional_where_clause {
150				for predicate in where_clause.predicates.iter() {
151					if let syn::WherePredicate::Type(where_ty) = predicate {
152						if let syn::Type::Path(ref predicate) = where_ty.bounded_ty {
153							if *predicate == ty_path {
154								bounds.extend(where_ty.bounds.clone().into_iter());
155							}
156						}
157					}
158				}
159			}
160
161			syn::WherePredicate::Type(syn::PredicateType {
162				lifetimes: None,
163				bounded_ty: syn::Type::Path(ty_path),
164				colon_token: <Token![:]>::default(),
165				bounds,
166			})
167		})
168		.collect()
169}
170
171/// Traverse the RPC trait by first finding the subscription parameters and then all elements
172/// needed for generating the `client` and `server` traits/implementations.
173fn visit_trait(item_trait: &syn::ItemTrait, sub_tys: &[syn::Type]) -> FindAllParams {
174	let type_params: HashSet<_> = item_trait.generics.type_params().map(|t| t.ident.clone()).collect();
175	let sub_tys = FindSubscriptionParams::new(type_params).visit(sub_tys);
176	let mut visitor = FindAllParams::new(sub_tys);
177	visitor.visit_item_trait(item_trait);
178	visitor
179}
180
181/// Checks whether provided type is an `Option<...>`.
182pub(crate) fn is_option(ty: &syn::Type) -> bool {
183	if let syn::Type::Path(path) = ty {
184		let mut it = path.path.segments.iter().peekable();
185		while let Some(seg) = it.next() {
186			// The leaf segment should be `Option` with or without angled brackets.
187			if seg.ident == "Option" && it.peek().is_none() {
188				return true;
189			}
190		}
191	}
192
193	false
194}
195
196/// Iterates over all Attribute's and parses only the attributes that are doc comments.
197///
198/// Note that `doc comments` are expanded into `#[doc = "some comment"]`
199/// Thus, if the attribute starts with `doc` => it's regarded as a doc comment.
200pub(crate) fn extract_doc_comments(attrs: &[syn::Attribute]) -> TokenStream2 {
201	let docs = attrs.iter().filter(|attr| match &attr.meta {
202		syn::Meta::NameValue(meta) => {
203			meta.path.is_ident("doc")
204				&& matches!(meta.value, syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(_), .. }))
205		}
206		_ => false,
207	});
208	quote! ( #(#docs)* )
209}
210
211#[cfg(test)]
212mod tests {
213	use super::is_option;
214	use syn::parse_quote;
215
216	#[test]
217	fn is_option_works() {
218		assert!(is_option(&parse_quote!(Option<T>)));
219		// could be a type alias.
220		assert!(is_option(&parse_quote!(Option)));
221		assert!(is_option(&parse_quote!(std::option::Option<R>)));
222		assert!(!is_option(&parse_quote!(foo::bar::Option::Booyah)));
223	}
224}