Skip to main content

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