Skip to main content

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::{
32    attributes::{optional, Aliases, Argument, AttributeMeta, MissingArgument, NameMapping},
33    helpers::extract_doc_comments,
34};
35use proc_macro2::TokenStream as TokenStream2;
36use quote::quote;
37use syn::{punctuated::Punctuated, spanned::Spanned, Attribute, Token};
38
39/// Represents a single argument in a RPC call.
40///
41/// stores modifications based on attributes
42#[derive(Debug, Clone)]
43pub struct RpcFnArg {
44    pub(crate) arg_pat: syn::PatIdent,
45    pub(crate) ty: syn::Type,
46}
47
48impl RpcFnArg {
49    /// Return the pattern identifier of the argument.
50    pub fn arg_pat(&self) -> &syn::PatIdent {
51        &self.arg_pat
52    }
53}
54
55#[derive(Debug, Clone)]
56pub struct RpcMethod {
57    pub name: String,
58    pub blocking: bool,
59    pub docs: TokenStream2,
60    pub params: Vec<RpcFnArg>,
61    pub signature: syn::TraitItemFn,
62    pub aliases: Vec<String>,
63}
64
65impl RpcMethod {
66    pub fn from_item(attr: Attribute, mut method: syn::TraitItemFn) -> syn::Result<Self> {
67        let [aliases, blocking, name] =
68            AttributeMeta::parse(attr)?.retain(["aliases", "blocking", "name"])?;
69
70        let aliases = parse_aliases(aliases)?;
71        let blocking = optional(blocking, Argument::flag)?.is_some();
72        let name = name?.string()?;
73
74        let docs = extract_doc_comments(&method.attrs);
75
76        if blocking && method.sig.asyncness.is_some() {
77            return Err(syn::Error::new(method.sig.span(), "Blocking method must be synchronous"));
78        }
79
80        let params: Vec<_> = method
81            .sig
82            .inputs
83            .iter()
84            .filter_map(|arg| match arg {
85                syn::FnArg::Receiver(_) => None,
86                syn::FnArg::Typed(arg) => match &*arg.pat {
87                    syn::Pat::Ident(name) => {
88                        Some(Ok(RpcFnArg { arg_pat: name.clone(), ty: (*arg.ty).clone() }))
89                    }
90                    syn::Pat::Wild(wild) => Some(Err(syn::Error::new(
91                        wild.underscore_token.span(),
92                        "Method argument names must be valid Rust identifiers; got `_` instead",
93                    ))),
94                    _ => Some(Err(syn::Error::new(
95                        arg.span(),
96                        format!("Unexpected method signature input; got {:?} ", *arg.pat),
97                    ))),
98                },
99            })
100            .collect::<Result<_, _>>()?;
101
102        // We've analyzed attributes and don't need them anymore.
103        method.attrs.clear();
104
105        Ok(Self { aliases, blocking, name, params, signature: method, docs })
106    }
107}
108
109#[derive(Debug, Clone)]
110pub struct RpcSubscription {
111    pub name: String,
112    /// When subscribing to an RPC, users can override the content of the `method` field
113    /// in the JSON data sent to subscribers.
114    /// Each subscription thus has one method name to set up the subscription,
115    /// one to unsubscribe and, optionally, a third method name used to describe the
116    /// payload (aka "notification") sent back from the server to subscribers.
117    /// If no override is provided, the subscription method name is used.
118    pub notif_name_override: Option<String>,
119    pub docs: TokenStream2,
120    pub unsubscribe: String,
121    pub params: Vec<RpcFnArg>,
122    pub item: syn::Type,
123    pub signature: syn::TraitItemFn,
124    pub aliases: Vec<String>,
125    pub unsubscribe_aliases: Vec<String>,
126}
127
128impl RpcSubscription {
129    pub fn from_item(attr: syn::Attribute, mut sub: syn::TraitItemFn) -> syn::Result<Self> {
130        let [aliases, item, name, unsubscribe, unsubscribe_aliases] = AttributeMeta::parse(attr)?
131            .retain([
132            "aliases",
133            "item",
134            "name",
135            "unsubscribe",
136            "unsubscribe_aliases",
137        ])?;
138
139        let aliases = parse_aliases(aliases)?;
140        let map = name?.value::<NameMapping>()?;
141        let name = map.name;
142        let notif_name_override = map.mapped;
143        let item = item?.value()?;
144        let unsubscribe_aliases = parse_aliases(unsubscribe_aliases)?;
145
146        let docs = extract_doc_comments(&sub.attrs);
147        let unsubscribe = match parse_subscribe(unsubscribe)? {
148            Some(unsub) => unsub,
149            None => build_unsubscribe_method(&name).unwrap_or_else(||
150                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"),
151            ),
152        };
153
154        let params: Vec<_> = sub
155            .sig
156            .inputs
157            .iter()
158            .filter_map(|arg| match arg {
159                syn::FnArg::Receiver(_) => None,
160                syn::FnArg::Typed(arg) => match &*arg.pat {
161                    syn::Pat::Ident(name) => {
162                        Some(RpcFnArg { arg_pat: name.clone(), ty: (*arg.ty).clone() })
163                    }
164                    _ => panic!("Identifier in signature must be an ident"),
165                },
166            })
167            .collect();
168
169        // We've analyzed attributes and don't need them anymore.
170        sub.attrs.clear();
171
172        Ok(Self {
173            name,
174            notif_name_override,
175            unsubscribe,
176            unsubscribe_aliases,
177            params,
178            item,
179            signature: sub,
180            aliases,
181            docs,
182        })
183    }
184}
185
186#[derive(Debug)]
187pub struct RpcDescription {
188    /// Path to the `jsonrpsee` server types part.
189    pub(crate) jsonrpsee_server_path: TokenStream2,
190    /// Optional prefix for RPC namespace.
191    pub(crate) namespace: Option<String>,
192    /// Optional separator between namespace and method name. Defaults to `_`.
193    pub(crate) namespace_separator: Option<String>,
194    /// Trait definition in which all the attributes were stripped.
195    pub(crate) trait_def: syn::ItemTrait,
196    /// List of RPC methods defined in the trait.
197    pub(crate) methods: Vec<RpcMethod>,
198    /// List of RPC subscriptions defined in the trait.
199    pub(crate) subscriptions: Vec<RpcSubscription>,
200    /// Optional user defined trait bounds for the server implementation.
201    pub(crate) server_bounds: Option<Punctuated<syn::WherePredicate, Token![,]>>,
202}
203
204impl RpcDescription {
205    pub fn from_item(attr: Attribute, mut item: syn::ItemTrait) -> syn::Result<Self> {
206        let [server, namespace, namespace_separator, server_bounds] = AttributeMeta::parse(attr)?
207            .retain([
208            "server",
209            "namespace",
210            "namespace_separator",
211            "server_bounds",
212        ])?;
213
214        // Only the server trait is generated, but the flag stays required so that the attribute
215        // spells out what it produces.
216        if optional(server, Argument::flag)?.is_none() {
217            return Err(syn::Error::new_spanned(
218                &item.ident,
219                "The 'server' attribute must be applied",
220            ));
221        }
222        let namespace = optional(namespace, Argument::string)?;
223        let namespace_separator = optional(namespace_separator, Argument::string)?;
224        let server_bounds = optional(server_bounds, Argument::group)?;
225
226        let Ok(jsonrpsee_server_path) = crate::helpers::find_jsonrpsee_server_crate() else {
227            return Err(syn::Error::new_spanned(
228                &item.ident,
229                "Unable to locate 'jsonrpsee' server dependency",
230            ));
231        };
232
233        item.attrs.clear(); // Remove RPC attributes.
234
235        let mut methods = Vec::new();
236        let mut subscriptions = Vec::new();
237
238        // Go through all the methods in the trait and collect methods and
239        // subscriptions.
240        for entry in item.items.iter() {
241            if let syn::TraitItem::Fn(method) = entry {
242                if method.sig.receiver().is_none() {
243                    return Err(syn::Error::new_spanned(
244                        &method.sig,
245                        "First argument of the trait must be '&self'",
246                    ));
247                }
248
249                let mut is_method = false;
250                let mut is_sub = false;
251                if let Some(attr) = find_attr(&method.attrs, "method") {
252                    is_method = true;
253
254                    let method_data = RpcMethod::from_item(attr.clone(), method.clone())?;
255
256                    methods.push(method_data);
257                }
258                if let Some(attr) = find_attr(&method.attrs, "subscription") {
259                    is_sub = true;
260                    if is_method {
261                        return Err(syn::Error::new_spanned(
262                            method,
263                            "Element cannot be both subscription and method at the same time",
264                        ));
265                    }
266
267                    let sub_data = RpcSubscription::from_item(attr.clone(), method.clone())?;
268                    subscriptions.push(sub_data);
269                }
270
271                if !is_method && !is_sub {
272                    return Err(syn::Error::new_spanned(
273                        method,
274                        "Methods must have either 'method' or 'subscription' attribute",
275                    ));
276                }
277            } else {
278                return Err(syn::Error::new_spanned(entry, "Only methods allowed in RPC traits"));
279            }
280        }
281
282        if methods.is_empty() && subscriptions.is_empty() {
283            return Err(syn::Error::new_spanned(&item, "RPC cannot be empty"));
284        }
285
286        Ok(Self {
287            jsonrpsee_server_path,
288            namespace,
289            namespace_separator,
290            trait_def: item,
291            methods,
292            subscriptions,
293            server_bounds,
294        })
295    }
296
297    pub fn render(self) -> Result<TokenStream2, syn::Error> {
298        self.render_server()
299    }
300
301    /// Formats the identifier as a path relative to the resolved
302    /// `jsonrpsee` server path.
303    pub(crate) fn jrps_server_item(&self, item: impl quote::ToTokens) -> TokenStream2 {
304        let jsonrpsee = &self.jsonrpsee_server_path;
305        quote! { #jsonrpsee::#item }
306    }
307
308    /// Based on the namespace and separator, renders the full name of the RPC method/subscription.
309    /// Examples:
310    /// For namespace `foo`, method `makeSpam`, and separator `_`, result will be `foo_makeSpam`.
311    /// For separator `.`, result will be `foo.makeSpam`.
312    /// For no namespace, returns just `makeSpam`.
313    pub(crate) fn rpc_identifier<'a>(&self, method: &'a str) -> Cow<'a, str> {
314        if let Some(ns) = &self.namespace {
315            let sep = self.namespace_separator.as_deref().unwrap_or("_");
316            format!("{ns}{sep}{method}").into()
317        } else {
318            Cow::Borrowed(method)
319        }
320    }
321}
322
323fn parse_aliases(arg: Result<Argument, MissingArgument>) -> syn::Result<Vec<String>> {
324    let aliases = optional(arg, Argument::value::<Aliases>)?;
325
326    Ok(aliases.map(|a| a.list.into_iter().map(|lit| lit.value()).collect()).unwrap_or_default())
327}
328
329fn parse_subscribe(arg: Result<Argument, MissingArgument>) -> syn::Result<Option<String>> {
330    let unsub = optional(arg, Argument::string)?;
331
332    Ok(unsub)
333}
334
335fn find_attr<'a>(attrs: &'a [Attribute], ident: &str) -> Option<&'a Attribute> {
336    attrs.iter().find(|a| a.path().is_ident(ident))
337}
338
339fn build_unsubscribe_method(method: &str) -> Option<String> {
340    method.strip_prefix("subscribe").map(|s| format!("unsubscribe{s}"))
341}