1use 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#[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}
260fn 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
334fn 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}