r2r_macros/
lib.rs

1use proc_macro2::TokenStream;
2use quote::{quote, quote_spanned};
3use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields};
4
5extern crate proc_macro;
6
7/// Derives RosParams trait for a structure to use it with
8/// `r2r::Node::make_derived_parameter_handler()`.
9#[proc_macro_derive(RosParams)]
10pub fn derive_r2r_params(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
11    // Parse the input tokens into a syntax tree.
12    let input = parse_macro_input!(input as DeriveInput);
13
14    // Used in the quasi-quotation below as `#name`.
15    let name = input.ident;
16
17    let register_calls = get_register_calls(&input.data);
18    let get_param_matches = param_matches_for(quote!(get_parameter(suffix)), &input.data);
19    let set_param_matches =
20        param_matches_for(quote!(set_parameter(suffix, param_val)), &input.data);
21    let check_param_matches =
22        param_matches_for(quote!(check_parameter(suffix, param_val)), &input.data);
23
24    let expanded = quote! {
25        // The generated impl.
26        impl ::r2r::RosParams for #name {
27            fn register_parameters(
28                &mut self,
29                prefix: &str,
30                desc: ::std::option::Option<::r2r::Parameter>,
31                params: &mut ::r2r::indexmap::IndexMap<String, ::r2r::Parameter>,
32            ) -> ::r2r::Result<()> {
33                let prefix = if prefix.is_empty() {
34                    String::from("")
35                } else {
36                    format!("{prefix}.")
37                };
38                #register_calls
39                Ok(())
40            }
41            fn get_parameter(&mut self, param_name: &str) -> ::r2r::Result<::r2r::ParameterValue>
42            {
43                let (prefix, suffix) = match param_name.split_once('.') {
44                    None => (param_name, ""),
45                    Some((prefix, suffix)) => (prefix, suffix)
46                };
47                let result = match prefix {
48                    #get_param_matches
49                    _ => Err(::r2r::Error::InvalidParameterName {
50                        name: "".into(),
51                    }),
52                };
53                result.map_err(|e| e.update_param_name(&param_name))
54            }
55            fn set_parameter(&mut self, param_name: &str, param_val: &::r2r::ParameterValue) -> ::r2r::Result<()>
56            {
57                let (prefix, suffix) = match param_name.split_once('.') {
58                    None => (param_name, ""),
59                    Some((prefix, suffix)) => (prefix, suffix)
60                };
61                let result = match prefix {
62                    #set_param_matches
63                    _ => Err(::r2r::Error::InvalidParameterName {
64                        name: "".into(),
65                    }),
66                };
67                result.map_err(|e| e.update_param_name(&param_name))
68            }
69            fn check_parameter(&self, param_name: &str, param_val: &::r2r::ParameterValue) -> ::r2r::Result<()>
70            {
71                let (prefix, suffix) = match param_name.split_once('.') {
72                    None => (param_name, ""),
73                    Some((prefix, suffix)) => (prefix, suffix)
74                };
75                let result = match prefix {
76                    #check_param_matches
77                    _ => Err(::r2r::Error::InvalidParameterName {
78                        name: "".into(),
79                    }),
80                };
81                result.map_err(|e| e.update_param_name(&param_name))
82            }
83        }
84    };
85
86    // Hand the output tokens back to the compiler.
87    proc_macro::TokenStream::from(expanded)
88}
89
90// Generate calls to register functions of struct fields
91fn get_register_calls(data: &Data) -> TokenStream {
92    match *data {
93        Data::Struct(ref data) => match data.fields {
94            Fields::Named(ref fields) => {
95                let field_matches = fields.named.iter().map(|f| {
96                    let name = &f.ident;
97                    let format_str = format!("{{prefix}}{}", name.as_ref().unwrap());
98                    let desc = get_field_doc(f);
99                    quote_spanned! {
100                        f.span() =>
101                            let param = ::r2r::Parameter {
102                                value: ::r2r::ParameterValue::NotSet, // will be set for leaf params by register_parameters() below
103                                description: #desc,
104                            };
105                            self.#name.register_parameters(&format!(#format_str), Some(param), params)?;
106                    }
107                });
108                quote! {
109                    #(#field_matches)*
110                }
111            }
112            _ => unimplemented!(),
113        },
114        Data::Enum(_) | Data::Union(_) => unimplemented!(),
115    }
116}
117
118fn get_field_doc(f: &syn::Field) -> String {
119    if let Some(doc) = f
120        .attrs
121        .iter()
122        .find(|&attr| attr.path().get_ident().is_some_and(|id| id == "doc"))
123    {
124        match &doc.meta.require_name_value().unwrap().value {
125            ::syn::Expr::Lit(exprlit) => match &exprlit.lit {
126                ::syn::Lit::Str(s) => s.value().trim().to_owned(),
127                _ => unimplemented!(),
128            },
129            _ => unimplemented!(),
130        }
131    } else {
132        "".to_string()
133    }
134}
135
136// Generate match arms for RosParams::update_parameters()
137fn param_matches_for(call: TokenStream, data: &Data) -> TokenStream {
138    match *data {
139        Data::Struct(ref data) => match data.fields {
140            Fields::Named(ref fields) => {
141                let field_matches = fields.named.iter().map(|f| {
142                    let name = &f.ident;
143                    let name_str = format!("{}", name.as_ref().unwrap());
144                    quote_spanned! {
145                        f.span() =>
146                            #name_str => {
147                                self.#name.#call
148                            }
149                    }
150                });
151                quote! {
152                    #(#field_matches)*
153                }
154            }
155            _ => unimplemented!(),
156        },
157        Data::Enum(_) | Data::Union(_) => unimplemented!(),
158    }
159}