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}