confetti_derive/
lib.rs

1extern crate proc_macro;
2use proc_macro::TokenStream;
3use quote::quote;
4use syn::spanned::Spanned;
5use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields, Lit, Meta, NestedMeta};
6
7/// Derives the FromConf and ToConf traits for struct types
8///
9/// This attribute allows a struct to be serialized to and deserialized from
10/// configuration format using the confetti-rs library.
11///
12/// # Example
13///
14/// ```rust
15/// use confetti_rs::ConfMap;
16///
17/// #[derive(ConfMap, Debug)]
18/// struct ServerConfig {
19///     port: i32,
20///     host: String,
21///     #[conf_map(name = "max-connections")]
22///     max_connections: Option<i32>,
23/// }
24/// ```
25///
26/// # Attributes
27///
28/// - `#[conf_map(name = "field-name")]`: Specify a custom name for the field in the configuration
29#[proc_macro_derive(ConfMap, attributes(conf_map))]
30pub fn derive_conf_map(input: TokenStream) -> TokenStream {
31    let input = parse_macro_input!(input as DeriveInput);
32    let name = &input.ident;
33    let name_str = name.to_string();
34
35    let (impl_from_conf, impl_to_conf) = match &input.data {
36        Data::Struct(data_struct) => {
37            match &data_struct.fields {
38                Fields::Named(fields_named) => {
39                    let from_conf_fields = fields_named.named.iter().map(|field| {
40                        let field_name = field.ident.as_ref().unwrap();
41                        let field_name_str = field_name.to_string();
42                        let field_type = &field.ty;
43
44                        // Check for conf_map attributes
45                        let conf_name = get_conf_name_from_attrs(&field.attrs, &field_name_str);
46                        let is_optional = is_option_type(field_type);
47
48                        if is_optional {
49                            quote! {
50                                #field_name: {
51                                    if let Some(child) = directive.children.iter().find(|d| d.name.value == #conf_name) {
52                                        if !child.arguments.is_empty() {
53                                            Some(confetti_rs::mapper::ValueConverter::from_conf_value(&child.arguments[0].value)?)
54                                        } else {
55                                            None
56                                        }
57                                    } else {
58                                        None
59                                    }
60                                }
61                            }
62                        } else {
63                            quote! {
64                                #field_name: {
65                                    if let Some(child) = directive.children.iter().find(|d| d.name.value == #conf_name) {
66                                        if !child.arguments.is_empty() {
67                                            confetti_rs::mapper::ValueConverter::from_conf_value(&child.arguments[0].value)?
68                                        } else {
69                                            return Err(confetti_rs::mapper::MapperError::MissingField(#conf_name.to_string()));
70                                        }
71                                    } else {
72                                        return Err(confetti_rs::mapper::MapperError::MissingField(#conf_name.to_string()));
73                                    }
74                                }
75                            }
76                        }
77                    });
78
79                    let to_conf_fields = fields_named.named.iter().map(|field| {
80                        let field_name = field.ident.as_ref().unwrap();
81                        let field_name_str = field_name.to_string();
82
83                        // Check for conf_map attributes
84                        let conf_name = get_conf_name_from_attrs(&field.attrs, &field_name_str);
85                        let is_optional = is_option_type(&field.ty);
86
87                        if is_optional {
88                            quote! {
89                                if let Some(value) = &self.#field_name {
90                                    let arg_value = confetti_rs::mapper::ValueConverter::to_conf_value(value)?;
91                                    // Use requires_quotes method
92                                    // Add ValueConverter trait directly to determine if quotes are needed
93                                    let is_quoted = confetti_rs::mapper::ValueConverter::requires_quotes(value);
94                                    let arg = confetti_rs::ConfArgument {
95                                        value: arg_value,
96                                        span: 0..0,
97                                        is_quoted: is_quoted,
98                                        is_triple_quoted: false,
99                                        is_expression: false,
100                                    };
101
102                                    let child = confetti_rs::ConfDirective {
103                                        name: confetti_rs::ConfArgument {
104                                            value: #conf_name.to_string(),
105                                            span: 0..0,
106                                            is_quoted: false,
107                                            is_triple_quoted: false,
108                                            is_expression: false,
109                                        },
110                                        arguments: vec![arg],
111                                        children: vec![],
112                                    };
113
114                                    children.push(child);
115                                }
116                            }
117                        } else {
118                            quote! {
119                                let arg_value = confetti_rs::mapper::ValueConverter::to_conf_value(&self.#field_name)?;
120                                // Use requires_quotes method
121                                // Add ValueConverter trait directly to determine if quotes are needed
122                                let is_quoted = confetti_rs::mapper::ValueConverter::requires_quotes(&self.#field_name);
123                                let arg = confetti_rs::ConfArgument {
124                                    value: arg_value,
125                                    span: 0..0,
126                                    is_quoted: is_quoted,
127                                    is_triple_quoted: false,
128                                    is_expression: false,
129                                };
130
131                                let child = confetti_rs::ConfDirective {
132                                    name: confetti_rs::ConfArgument {
133                                        value: #conf_name.to_string(),
134                                        span: 0..0,
135                                        is_quoted: false,
136                                        is_triple_quoted: false,
137                                        is_expression: false,
138                                    },
139                                    arguments: vec![arg],
140                                    children: vec![],
141                                };
142
143                                children.push(child);
144                            }
145                        }
146                    });
147
148                    let from_impl = quote! {
149                        impl confetti_rs::FromConf for #name {
150                            fn from_directive(directive: &confetti_rs::ConfDirective) -> Result<Self, confetti_rs::MapperError> {
151                                if directive.name.value != #name_str {
152                                    return Err(confetti_rs::MapperError::ParseError(
153                                        format!("Expected directive name {}, found {}", #name_str, directive.name.value)
154                                    ));
155                                }
156
157                                Ok(Self {
158                                    #(#from_conf_fields),*
159                                })
160                            }
161                        }
162                    };
163
164                    let to_impl = quote! {
165                        impl confetti_rs::ToConf for #name {
166                            fn to_directive(&self) -> Result<confetti_rs::ConfDirective, confetti_rs::MapperError> {
167                                let mut children = Vec::new();
168
169                                #(#to_conf_fields)*
170
171                                Ok(confetti_rs::ConfDirective {
172                                    name: confetti_rs::ConfArgument {
173                                        value: #name_str.to_string(),
174                                        span: 0..0,
175                                        is_quoted: false,
176                                        is_triple_quoted: false,
177                                        is_expression: false,
178                                    },
179                                    arguments: vec![],
180                                    children,
181                                })
182                            }
183                        }
184                    };
185
186                    (from_impl, to_impl)
187                }
188                _ => {
189                    // Only supports named fields
190                    return syn::Error::new(
191                        data_struct.fields.span(),
192                        "ConfMap can only be derived for structs with named fields",
193                    )
194                    .to_compile_error()
195                    .into();
196                }
197            }
198        }
199        _ => {
200            // Only supports structs
201            return syn::Error::new(input.span(), "ConfMap can only be derived for structs")
202                .to_compile_error()
203                .into();
204        }
205    };
206
207    let expanded = quote! {
208        #impl_from_conf
209
210        #impl_to_conf
211    };
212
213    expanded.into()
214}
215
216// Helper functions
217
218fn get_conf_name_from_attrs(attrs: &[Attribute], default_name: &str) -> String {
219    for attr in attrs {
220        if attr.path.is_ident("conf_map") {
221            if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
222                for nested_meta in meta_list.nested.iter() {
223                    if let NestedMeta::Meta(Meta::NameValue(name_value)) = nested_meta {
224                        if name_value.path.is_ident("name") {
225                            if let Lit::Str(lit_str) = &name_value.lit {
226                                return lit_str.value();
227                            }
228                        }
229                    }
230                }
231            }
232        }
233    }
234
235    // Return the field name as default
236    default_name.to_string()
237}
238
239fn is_option_type(ty: &syn::Type) -> bool {
240    if let syn::Type::Path(type_path) = ty {
241        if let Some(segment) = type_path.path.segments.last() {
242            return segment.ident == "Option";
243        }
244    }
245    false
246}