error_forge_derive/
lib.rs

1extern crate proc_macro;
2use proc_macro::TokenStream;
3use quote::{quote, format_ident};
4use syn::{parse_macro_input, DeriveInput, Data, Fields};
5
6/// Derive macro for ModError
7///
8/// This macro automatically implements the ForgeError trait and common
9/// error handling functionality for a struct or enum, allowing for
10/// "lazy mode" error creation with minimal boilerplate.
11///
12/// # Example
13/// 
14/// When the macro is used in your application where error-forge is a dependency:
15/// 
16/// ```ignore
17/// use error_forge::ModError;
18/// 
19/// #[derive(Debug, ModError)]
20/// #[error_prefix("Database")]
21/// pub enum DbError {
22///     #[error_display("Connection to {0} failed")]
23///     ConnectionFailed(String),
24/// 
25///     #[error_display("Query execution failed: {reason}")]
26///     QueryFailed { reason: String },
27/// 
28///     #[error_display("Transaction error")]
29///     #[error_http_status(400)]
30///     TransactionError,
31/// }
32/// ```
33///
34/// Note: This is a procedural macro that is re-exported by the `error-forge` crate.
35/// When using in your application, import it from the main crate with `use error_forge::ModError;`.
36#[proc_macro_derive(ModError, attributes(error_prefix, error_display, error_kind,
37                                         error_caption, error_retryable, error_http_status,
38                                         error_exit_code))]
39pub fn derive_mod_error(input: TokenStream) -> TokenStream {
40    // Parse the input
41    let input = parse_macro_input!(input as DeriveInput);
42    
43    // Check if this is an enum or struct
44    let is_enum = match &input.data {
45        Data::Enum(_) => true,
46        Data::Struct(_) => false,
47        Data::Union(_) => panic!("ModError cannot be derived for unions"),
48    };
49    
50    // Get the error prefix from attributes
51    let error_prefix = get_error_prefix(&input.attrs);
52    
53    // Generate implementation based on whether it's an enum or struct
54    let implementation = if is_enum {
55        implement_for_enum(&input, &error_prefix)
56    } else {
57        implement_for_struct(&input, &error_prefix)
58    };
59    
60    // Return the generated implementation
61    TokenStream::from(implementation)
62}
63
64// Extract error_prefix attribute value
65fn get_error_prefix(attrs: &[syn::Attribute]) -> String {
66    for attr in attrs {
67        if attr.path.is_ident("error_prefix") {
68            // Try both attribute formats
69            // Format: #[error_prefix = "text"]
70            if let Ok(syn::Meta::NameValue(meta)) = attr.parse_meta() {
71                if let syn::Lit::Str(lit) = meta.lit {
72                    return lit.value();
73                }
74            }
75            // Format: #[error_prefix("text")]
76            else if let Ok(syn::Meta::List(meta)) = attr.parse_meta() {
77                if let Some(nested) = meta.nested.iter().next() {
78                    if let syn::NestedMeta::Lit(syn::Lit::Str(lit)) = nested {
79                        return lit.value();
80                    }
81                }
82            }
83        }
84    }
85    String::new()
86}
87
88// Implement ModError for an enum
89fn implement_for_enum(input: &DeriveInput, error_prefix: &str) -> proc_macro2::TokenStream {
90    let name = &input.ident;
91    let data_enum = match &input.data {
92        Data::Enum(data) => data,
93        _ => panic!("Expected enum"),
94    };
95    
96    // Generate match arms for each variant
97    let mut kind_match_arms = Vec::new();
98    let mut caption_match_arms = Vec::new();
99    let mut display_match_arms = Vec::new();
100    let mut retryable_match_arms = Vec::new();
101    let mut status_code_match_arms = Vec::new();
102    let mut exit_code_match_arms = Vec::new();
103    
104    // Process each variant
105    for variant in &data_enum.variants {
106        let variant_name = &variant.ident;
107        let variant_name_str = variant_name.to_string();
108        
109        // Default values
110        let mut display_format = variant_name_str.clone();
111        let mut retryable = false;
112        let mut status_code: u16 = 500;
113        let mut exit_code: i32 = 1;
114        
115        // Extract attributes
116        for attr in &variant.attrs {
117            if attr.path.is_ident("error_display") {
118                if let Ok(syn::Meta::NameValue(meta)) = attr.parse_meta() {
119                    if let syn::Lit::Str(lit) = meta.lit {
120                        display_format = lit.value();
121                    }
122                }
123            } else if attr.path.is_ident("error_retryable") {
124                retryable = true;
125            } else if attr.path.is_ident("error_http_status") {
126                if let Ok(syn::Meta::NameValue(meta)) = attr.parse_meta() {
127                    if let syn::Lit::Int(lit) = meta.lit {
128                        status_code = lit.base10_parse().unwrap_or(500);
129                    }
130                }
131            } else if attr.path.is_ident("error_exit_code") {
132                if let Ok(syn::Meta::NameValue(meta)) = attr.parse_meta() {
133                    if let syn::Lit::Int(lit) = meta.lit {
134                        exit_code = lit.base10_parse().unwrap_or(1);
135                    }
136                }
137            }
138        }
139        
140        // Generate pattern matching based on the variant's fields
141        match &variant.fields {
142            Fields::Named(fields) => {
143                let field_names: Vec<_> = fields.named.iter()
144                    .map(|f| f.ident.as_ref().unwrap())
145                    .collect();
146                
147                // Format string handled directly in match arm
148                
149                kind_match_arms.push(quote! {
150                    Self::#variant_name { .. } => #variant_name_str
151                });
152                
153                caption_match_arms.push(quote! {
154                    Self::#variant_name { .. } => concat!(#error_prefix, ": Error")
155                });
156                
157                let _field_patterns = field_names.iter().map(|name| {
158                    let _name_str = name.to_string();
159                    quote! { #name, }
160                });
161                
162                // For struct variants, create a properly formatted string without using fields
163                display_match_arms.push(quote! {
164                    Self::#variant_name { .. } => format!("{}: {}", #error_prefix, #display_format)
165                });
166                
167                retryable_match_arms.push(quote! {
168                    Self::#variant_name { .. } => #retryable
169                });
170                
171                status_code_match_arms.push(quote! {
172                    Self::#variant_name { .. } => #status_code
173                });
174                
175                exit_code_match_arms.push(quote! {
176                    Self::#variant_name { .. } => #exit_code
177                });
178            },
179            Fields::Unnamed(fields) => {
180                let field_count = fields.unnamed.len();
181                let field_names: Vec<_> = (0..field_count)
182                    .map(|i| format_ident!("_{}", i))
183                    .collect();
184                
185                // Generate display format with tuple fields
186                // field_names is already Vec<Ident> so we can pass it directly
187                // Format string handled directly in match arm
188                
189                kind_match_arms.push(quote! {
190                    Self::#variant_name(..) => #variant_name_str
191                });
192                
193                caption_match_arms.push(quote! {
194                    Self::#variant_name(..) => concat!(#error_prefix, ": Error")
195                });
196                
197                let _field_patterns = field_names.iter().map(|name| {
198                    quote! { #name, }
199                });
200                
201                // For tuple variants, handle simple positional formatting for {0}, {1}, etc.
202                if display_format.contains("{0}") || display_format.contains("{}") {
203                    // Recreate the field pattern list here to avoid conflicts with renamed variables
204                    let field_pattern_list = field_names.iter().map(|name| quote! { #name, });
205                    display_match_arms.push(quote! {
206                        Self::#variant_name(#(#field_pattern_list)*) => format!("{}: {}", #error_prefix, format!(#display_format #(, #field_names)*))
207                    });
208                } else {
209                    // Fall back to simple display if no formatting placeholders
210                    display_match_arms.push(quote! {
211                        Self::#variant_name(..) => format!("{}: {}", #error_prefix, #display_format)
212                    });
213                }
214                
215                retryable_match_arms.push(quote! {
216                    Self::#variant_name(..) => #retryable
217                });
218                
219                status_code_match_arms.push(quote! {
220                    Self::#variant_name(..) => #status_code
221                });
222                
223                exit_code_match_arms.push(quote! {
224                    Self::#variant_name(..) => #exit_code
225                });
226            },
227            Fields::Unit => {
228                // Unit variant (no fields)
229                kind_match_arms.push(quote! {
230                    Self::#variant_name => #variant_name_str
231                });
232                
233                caption_match_arms.push(quote! {
234                    Self::#variant_name => concat!(#error_prefix, ": Error")
235                });
236                
237                display_match_arms.push(quote! {
238                    Self::#variant_name => format!("{}: {}", #error_prefix, #display_format)
239                });
240                
241                retryable_match_arms.push(quote! {
242                    Self::#variant_name => #retryable
243                });
244                
245                status_code_match_arms.push(quote! {
246                    Self::#variant_name => #status_code
247                });
248                
249                exit_code_match_arms.push(quote! {
250                    Self::#variant_name => #exit_code
251                });
252            },
253        }
254    }
255    
256    // Generate implementation
257    quote! {
258        impl ::std::fmt::Display for #name {
259            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
260                let msg = match self {
261                    #(#display_match_arms,)*
262                };
263                write!(f, "{}", msg)
264            }
265        }
266        
267        impl ::error_forge::error::ForgeError for #name {
268            fn kind(&self) -> &'static str {
269                match self {
270                    #(#kind_match_arms,)*
271                }
272            }
273            
274            fn caption(&self) -> &'static str {
275                match self {
276                    #(#caption_match_arms,)*
277                }
278            }
279            
280            fn is_retryable(&self) -> bool {
281                match self {
282                    #(#retryable_match_arms,)*
283                }
284            }
285            
286            fn status_code(&self) -> u16 {
287                match self {
288                    #(#status_code_match_arms,)*
289                }
290            }
291            
292            fn exit_code(&self) -> i32 {
293                match self {
294                    #(#exit_code_match_arms,)*
295                }
296            }
297        }
298        
299        impl ::std::error::Error for #name {
300            fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> {
301                None
302            }
303        }
304    }
305}
306
307// Implement ModError for a struct
308fn implement_for_struct(input: &DeriveInput, error_prefix: &str) -> proc_macro2::TokenStream {
309    let name = &input.ident;
310    let name_str = name.to_string();
311    
312    quote! {
313        impl ::std::fmt::Display for #name {
314            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
315                write!(f, "{}: Error", #error_prefix)
316            }
317        }
318        
319        impl ::error_forge::error::ForgeError for #name {
320            fn kind(&self) -> &'static str {
321                #name_str
322            }
323            
324            fn caption(&self) -> &'static str {
325                concat!(#error_prefix, ": Error")
326            }
327        }
328        
329        impl ::std::error::Error for #name {
330            fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> {
331                None
332            }
333        }
334    }
335}
336
337// Note: The implementation now handles formatting directly in the match arms instead of using a helper function