sui_open_rpc_macros/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use proc_macro::TokenStream;
5
6use derive_syn_parse::Parse;
7use itertools::Itertools;
8use proc_macro2::{Ident, TokenTree};
9use proc_macro2::{Span, TokenStream as TokenStream2};
10use quote::{ToTokens, TokenStreamExt, quote};
11use syn::parse::{Parse, ParseStream};
12use syn::punctuated::Punctuated;
13use syn::spanned::Spanned;
14use syn::token::{Comma, Paren};
15use syn::{
16    Attribute, GenericArgument, LitStr, PatType, Path, PathArguments, Token, TraitItem, Type,
17    parse, parse_macro_input,
18};
19use unescape::unescape;
20
21const SUI_RPC_ATTRS: [&str; 2] = ["deprecated", "version"];
22
23/// Add a [Service name]OpenRpc struct and implementation providing access to Open RPC doc builder.
24/// This proc macro must be use in conjunction with `jsonrpsee_proc_macro::rpc`
25///
26/// The generated method `open_rpc` is added to [Service name]OpenRpc,
27/// ideally we want to add this to the trait generated by jsonrpsee framework, creating a new struct
28/// to provide access to the method is a workaround.
29///
30/// TODO: consider contributing the open rpc doc macro to jsonrpsee to simplify the logics.
31#[proc_macro_attribute]
32pub fn open_rpc(attr: TokenStream, item: TokenStream) -> TokenStream {
33    let attr: OpenRpcAttributes = parse_macro_input!(attr);
34
35    let mut trait_data: syn::ItemTrait = syn::parse(item).unwrap();
36    let rpc_definition = parse_rpc_method(&mut trait_data).unwrap();
37
38    let namespace = attr
39        .find_attr("namespace")
40        .map(|str| str.value())
41        .unwrap_or_default();
42
43    let tag = attr.find_attr("tag").to_quote();
44
45    let methods = rpc_definition.methods.iter().flat_map(|method|{
46        if method.deprecated {
47            return None;
48        }
49        let name = &method.name;
50        let deprecated = method.deprecated;
51        let doc = &method.doc;
52        let mut inputs = Vec::new();
53        for (name, ty, description) in &method.params {
54            let (ty, required) = extract_type_from_option(ty.clone());
55            let description = if let Some(description) = description {
56                quote! {Some(#description.to_string())}
57            } else {
58                quote! {None}
59            };
60
61            inputs.push(quote! {
62                let des = builder.create_content_descriptor::<#ty>(#name, None, #description, #required);
63                inputs.push(des);
64            })
65        }
66        let returns_ty = if let Some(ty) = &method.returns {
67            let (ty, required) = extract_type_from_option(ty.clone());
68            let name = quote! {#ty}.to_string();
69            quote! {Some(builder.create_content_descriptor::<#ty>(#name, None, None, #required));}
70        } else {
71            quote! {None;}
72        };
73
74        if method.is_pubsub {
75            Some(quote! {
76                let mut inputs: Vec<sui_open_rpc::ContentDescriptor> = Vec::new();
77                #(#inputs)*
78                let result = #returns_ty
79                builder.add_subscription(#namespace, #name, inputs, result, #doc, #tag, #deprecated);
80            })
81        } else {
82            Some(quote! {
83                let mut inputs: Vec<sui_open_rpc::ContentDescriptor> = Vec::new();
84                #(#inputs)*
85                let result = #returns_ty
86                builder.add_method(#namespace, #name, inputs, result, #doc, #tag, #deprecated);
87            })
88        }
89    }).collect::<Vec<_>>();
90
91    let routes = rpc_definition
92        .version_routing
93        .into_iter()
94        .map(|route| {
95            let name = route.name;
96            let route_to = route.route_to;
97            let comparator = route.token.to_string();
98            let version = route.version;
99            quote! {
100                builder.add_method_routing(#namespace, #name, #route_to, #comparator, #version);
101            }
102        })
103        .collect::<Vec<_>>();
104
105    let open_rpc_name = quote::format_ident!("{}OpenRpc", &rpc_definition.name);
106
107    quote! {
108        #trait_data
109        pub struct #open_rpc_name;
110        impl #open_rpc_name {
111            pub fn module_doc() -> sui_open_rpc::Module{
112                let mut builder = sui_open_rpc::RpcModuleDocBuilder::default();
113                #(#methods)*
114                #(#routes)*
115                builder.build()
116            }
117        }
118    }
119    .into()
120}
121
122trait OptionalQuote {
123    fn to_quote(&self) -> TokenStream2;
124}
125
126impl OptionalQuote for Option<LitStr> {
127    fn to_quote(&self) -> TokenStream2 {
128        if let Some(value) = self {
129            quote!(Some(#value.to_string()))
130        } else {
131            quote!(None)
132        }
133    }
134}
135
136struct RpcDefinition {
137    name: Ident,
138    methods: Vec<Method>,
139    version_routing: Vec<Routing>,
140}
141struct Method {
142    name: String,
143    params: Vec<(String, Type, Option<String>)>,
144    returns: Option<Type>,
145    doc: String,
146    is_pubsub: bool,
147    deprecated: bool,
148}
149struct Routing {
150    name: String,
151    route_to: String,
152    token: TokenStream2,
153    version: String,
154}
155
156fn parse_rpc_method(trait_data: &mut syn::ItemTrait) -> Result<RpcDefinition, syn::Error> {
157    let mut methods = Vec::new();
158    let mut version_routing = Vec::new();
159    for trait_item in &mut trait_data.items {
160        if let TraitItem::Method(method) = trait_item {
161            let doc = extract_doc_comments(&method.attrs).to_string();
162            let params: Vec<_> = method
163                .sig
164                .inputs
165                .iter_mut()
166                .filter_map(|arg| {
167                    match arg {
168                        syn::FnArg::Receiver(_) => None,
169                        syn::FnArg::Typed(arg) => {
170                            let description = if let Some(description) = arg.attrs.iter().position(|a|a.path.is_ident("doc")){
171                                let doc = extract_doc_comments(&arg.attrs);
172                                arg.attrs.remove(description);
173                                Some(doc)
174                            }else{
175                                None
176                            };
177                            match *arg.pat.clone() {
178                                syn::Pat::Ident(name) => {
179                                    Some(get_type(arg).map(|ty| (name.ident.to_string(), ty, description)))
180                                }
181                                syn::Pat::Wild(wild) => Some(Err(syn::Error::new(
182                                    wild.underscore_token.span(),
183                                    "Method argument names must be valid Rust identifiers; got `_` instead",
184                                ))),
185                                _ => Some(Err(syn::Error::new(
186                                    arg.span(),
187                                    format!("Unexpected method signature input; got {:?} ", *arg.pat),
188                                ))),
189                            }
190                        },
191                    }
192                })
193                .collect::<Result<_, _>>()?;
194
195            let (method_name, returns, is_pubsub, deprecated) = if let Some(attr) =
196                find_attr(&mut method.attrs, "method")
197            {
198                let token: TokenStream = attr.tokens.clone().into();
199                let returns = match &method.sig.output {
200                    syn::ReturnType::Default => None,
201                    syn::ReturnType::Type(_, output) => extract_type_from(output, "RpcResult"),
202                };
203                let mut attributes = parse::<Attributes>(token)?;
204                let method_name = attributes.get_value("name");
205
206                let deprecated = attributes.find("deprecated").is_some();
207
208                if let Some(version_attr) = attributes.find("version")
209                    && let (Some(token), Some(version)) = (&version_attr.token, &version_attr.value)
210                {
211                    let route_to = format!("{method_name}_{}", version.value().replace('.', "_"));
212                    version_routing.push(Routing {
213                        name: method_name,
214                        route_to: route_to.clone(),
215                        token: token.to_token_stream(),
216                        version: version.value(),
217                    });
218                    if let Some(name) = attributes.find_mut("name") {
219                        name.value
220                            .replace(LitStr::new(&route_to, Span::call_site()));
221                    }
222                    attr.tokens = remove_sui_rpc_attributes(attributes);
223                    continue;
224                }
225                attr.tokens = remove_sui_rpc_attributes(attributes);
226                (method_name, returns, false, deprecated)
227            } else if let Some(attr) = find_attr(&mut method.attrs, "subscription") {
228                let token: TokenStream = attr.tokens.clone().into();
229                let attributes = parse::<Attributes>(token)?;
230                let name = attributes.get_value("name");
231                let type_ = attributes
232                    .find("item")
233                    .expect("Subscription should have a [item] attribute")
234                    .type_
235                    .clone()
236                    .expect("[item] attribute should have a value");
237                let deprecated = attributes.find("deprecated").is_some();
238                attr.tokens = remove_sui_rpc_attributes(attributes);
239                (name, Some(type_), true, deprecated)
240            } else {
241                panic!("Unknown method name")
242            };
243
244            methods.push(Method {
245                name: method_name,
246                params,
247                returns,
248                doc,
249                is_pubsub,
250                deprecated,
251            });
252        }
253    }
254    Ok(RpcDefinition {
255        name: trait_data.ident.clone(),
256        methods,
257        version_routing,
258    })
259}
260// Remove Sui rpc specific attributes.
261fn remove_sui_rpc_attributes(attributes: Attributes) -> TokenStream2 {
262    let attrs = attributes
263        .attrs
264        .into_iter()
265        .filter(|r| !SUI_RPC_ATTRS.contains(&r.key.to_string().as_str()))
266        .collect::<Punctuated<Attr, Comma>>();
267    quote! {(#attrs)}
268}
269
270fn extract_type_from(ty: &Type, from_ty: &str) -> Option<Type> {
271    fn path_is(path: &Path, from_ty: &str) -> bool {
272        path.leading_colon.is_none()
273            && path.segments.len() == 1
274            && path.segments.iter().next().unwrap().ident == from_ty
275    }
276
277    if let Type::Path(p) = ty
278        && p.qself.is_none()
279        && path_is(&p.path, from_ty)
280        && let PathArguments::AngleBracketed(a) = &p.path.segments[0].arguments
281        && let Some(GenericArgument::Type(ty)) = a.args.first()
282    {
283        return Some(ty.clone());
284    }
285    None
286}
287
288fn extract_type_from_option(ty: Type) -> (Type, bool) {
289    if let Some(ty) = extract_type_from(&ty, "Option") {
290        (ty, false)
291    } else {
292        (ty, true)
293    }
294}
295
296fn get_type(pat_type: &mut PatType) -> Result<Type, syn::Error> {
297    Ok(
298        if let Some((pos, attr)) = pat_type
299            .attrs
300            .iter()
301            .find_position(|a| a.path.is_ident("schemars"))
302        {
303            let attribute = parse::<NamedAttribute>(attr.tokens.clone().into())?;
304
305            let stream = syn::parse_str(&attribute.value.value())?;
306            let tokens = respan_token_stream(stream, attribute.value.span());
307
308            let path = syn::parse2(tokens)?;
309            pat_type.attrs.remove(pos);
310            path
311        } else {
312            pat_type.ty.as_ref().clone()
313        },
314    )
315}
316
317fn find_attr<'a>(attrs: &'a mut [Attribute], ident: &str) -> Option<&'a mut Attribute> {
318    attrs.iter_mut().find(|a| a.path.is_ident(ident))
319}
320
321fn respan_token_stream(stream: TokenStream2, span: Span) -> TokenStream2 {
322    stream
323        .into_iter()
324        .map(|mut token| {
325            if let TokenTree::Group(g) = &mut token {
326                *g = proc_macro2::Group::new(g.delimiter(), respan_token_stream(g.stream(), span));
327            }
328            token.set_span(span);
329            token
330        })
331        .collect()
332}
333
334/// Find doc comments by looking for #[doc = "..."] attributes.
335///
336/// Consecutive attributes are combined together. If there is a leading space, it will be removed,
337/// and if there is trailing whitespace it will also be removed. Single newlines in doc comments
338/// are replaced by spaces (soft wrapping), but double newlines (an empty line) are preserved.
339fn extract_doc_comments(attrs: &[Attribute]) -> String {
340    let mut s = String::new();
341    let mut sep = "";
342    for attr in attrs {
343        if !attr.path.is_ident("doc") {
344            continue;
345        }
346
347        let Ok(syn::Meta::NameValue(meta)) = attr.parse_meta() else {
348            continue;
349        };
350
351        let syn::Lit::Str(lit) = &meta.lit else {
352            continue;
353        };
354
355        let token = lit.value();
356        let line = token.strip_prefix(" ").unwrap_or(&token).trim_end();
357
358        if line.is_empty() {
359            s.push_str("\n\n");
360            sep = "";
361        } else {
362            s.push_str(sep);
363            sep = " ";
364        }
365
366        s.push_str(line);
367    }
368
369    unescape(&s).unwrap_or_else(|| panic!("Cannot unescape doc comments : [{s}]"))
370}
371
372#[derive(Parse, Debug)]
373struct OpenRpcAttributes {
374    #[parse_terminated(OpenRpcAttribute::parse)]
375    fields: Punctuated<OpenRpcAttribute, Token![,]>,
376}
377
378impl OpenRpcAttributes {
379    fn find_attr(&self, name: &str) -> Option<LitStr> {
380        self.fields
381            .iter()
382            .find(|attr| attr.label == name)
383            .map(|attr| attr.value.clone())
384    }
385}
386
387#[derive(Parse, Debug)]
388struct OpenRpcAttribute {
389    label: Ident,
390    _eq_token: Token![=],
391    value: syn::LitStr,
392}
393
394#[derive(Parse, Debug)]
395struct NamedAttribute {
396    #[paren]
397    _paren_token: Paren,
398    #[inside(_paren_token)]
399    _ident: Ident,
400    #[inside(_paren_token)]
401    _eq_token: Token![=],
402    #[inside(_paren_token)]
403    value: syn::LitStr,
404}
405
406#[derive(Debug)]
407struct Attributes {
408    pub attrs: Punctuated<Attr, syn::token::Comma>,
409}
410
411impl Attributes {
412    pub fn find(&self, attr_name: &str) -> Option<&Attr> {
413        self.attrs.iter().find(|attr| attr.key == attr_name)
414    }
415    pub fn find_mut(&mut self, attr_name: &str) -> Option<&mut Attr> {
416        self.attrs.iter_mut().find(|attr| attr.key == attr_name)
417    }
418    pub fn get_value(&self, attr_name: &str) -> String {
419        self.attrs
420            .iter()
421            .find(|attr| attr.key == attr_name)
422            .unwrap_or_else(|| panic!("Method should have a [{attr_name}] attribute."))
423            .value
424            .as_ref()
425            .unwrap_or_else(|| panic!("[{attr_name}] attribute should have a value"))
426            .value()
427    }
428}
429
430impl Parse for Attributes {
431    fn parse(input: ParseStream) -> syn::Result<Self> {
432        let content;
433        let _paren = syn::parenthesized!(content in input);
434        let attrs = content.parse_terminated(Attr::parse)?;
435        Ok(Self { attrs })
436    }
437}
438
439#[derive(Debug)]
440struct Attr {
441    pub key: Ident,
442    pub token: Option<TokenStream2>,
443    pub value: Option<syn::LitStr>,
444    pub type_: Option<Type>,
445}
446
447impl ToTokens for Attr {
448    fn to_tokens(&self, tokens: &mut TokenStream2) {
449        tokens.append(self.key.clone());
450        if let Some(token) = &self.token {
451            tokens.extend(token.to_token_stream());
452        }
453        if let Some(value) = &self.value {
454            tokens.append(value.token());
455        }
456        if let Some(type_) = &self.type_ {
457            tokens.extend(type_.to_token_stream());
458        }
459    }
460}
461
462impl Parse for Attr {
463    fn parse(input: ParseStream) -> syn::Result<Self> {
464        let key = input.parse()?;
465        let token = if input.peek(Token!(=)) {
466            Some(input.parse::<Token!(=)>()?.to_token_stream())
467        } else if input.peek(Token!(<=)) {
468            Some(input.parse::<Token!(<=)>()?.to_token_stream())
469        } else {
470            None
471        };
472
473        let value = if token.is_some() && input.peek(syn::LitStr) {
474            Some(input.parse::<syn::LitStr>()?)
475        } else {
476            None
477        };
478
479        let type_ = if token.is_some() && input.peek(syn::Ident) {
480            Some(input.parse::<Type>()?)
481        } else {
482            None
483        };
484
485        Ok(Self {
486            key,
487            token,
488            value,
489            type_,
490        })
491    }
492}