error_forge/
macros.rs

1/// Error hook types for centralized error handling
2#[derive(Clone, Copy, PartialEq, Eq, Debug)]
3pub enum ErrorLevel {
4    /// Debug-level errors (for detailed debugging)
5    Debug,
6    /// Information-level errors (least severe)
7    Info,
8    /// Warning-level errors (moderate severity)
9    Warning,
10    /// Error-level errors (high severity)
11    Error,
12    /// Critical-level errors (most severe)
13    Critical,
14}
15
16/// Error context passed to registered hooks
17pub struct ErrorContext<'a> {
18    /// The error caption
19    pub caption: &'a str,
20    /// The error kind
21    pub kind: &'a str,
22    /// The error level
23    pub level: ErrorLevel,
24    /// Whether the error is fatal
25    pub is_fatal: bool,
26    /// Whether the error can be retried
27    pub is_retryable: bool,
28}
29
30use std::sync::OnceLock;
31
32/// Global error hook for centralized error handling
33static ERROR_HOOK: OnceLock<fn(ErrorContext)> = OnceLock::new();
34
35/// Register a callback function to be called when errors are created
36/// 
37/// # Example
38/// 
39/// ```rust
40/// use error_forge::{AppError, macros::{register_error_hook, ErrorLevel, ErrorContext}};
41/// 
42/// // Setup logging with different levels
43/// register_error_hook(|ctx| {
44///     match ctx.level {
45///         ErrorLevel::Debug => println!("DEBUG: {} ({})", ctx.caption, ctx.kind),
46///         ErrorLevel::Info => println!("INFO: {} ({})", ctx.caption, ctx.kind),
47///         ErrorLevel::Warning => println!("WARNING: {} ({})", ctx.caption, ctx.kind),
48///         ErrorLevel::Error => println!("ERROR: {} ({})", ctx.caption, ctx.kind),
49///         ErrorLevel::Critical => println!("CRITICAL: {} ({})", ctx.caption, ctx.kind),
50///     }
51///     
52///     // Optional: send notifications for critical errors
53///     if ctx.level == ErrorLevel::Critical || ctx.is_fatal {
54///         // send_notification("Critical error occurred", ctx.caption);
55///     }
56/// });
57/// ```
58/// 
59pub fn register_error_hook(callback: fn(ErrorContext)) {
60    let _ = ERROR_HOOK.set(callback);
61}
62
63/// Call the registered error hook with error context if one is registered
64#[doc(hidden)]
65pub fn call_error_hook(caption: &str, kind: &str, is_fatal: bool, is_retryable: bool) {
66    if let Some(hook) = ERROR_HOOK.get() {
67        // Determine error level based on error properties
68        let level = if is_fatal {
69            ErrorLevel::Critical
70        } else if !is_retryable {
71            ErrorLevel::Error
72        } else if kind == "Warning" {
73            ErrorLevel::Warning
74        } else if kind == "Debug" {
75            ErrorLevel::Debug
76        } else {
77            ErrorLevel::Info
78        };
79        
80        hook(ErrorContext {
81            caption,
82            kind,
83            level,
84            is_fatal,
85            is_retryable,
86        });
87    }
88}
89
90#[macro_export]
91macro_rules! define_errors {
92    (
93        $(
94            $(#[$meta:meta])* $vis:vis enum $name:ident {
95                $( 
96                   $(#[error(display = $display:literal $(, $($display_param:ident),* )?)])?
97                   #[kind($kind:ident $(, $($tag:ident = $val:expr),* )?)]
98                   $variant:ident $( { $($field:ident : $ftype:ty),* $(,)? } )?, )*
99            }
100        )*
101    ) => {
102        $(
103            $(#[$meta])* #[derive(Debug)]
104            #[cfg_attr(feature = "serde", derive(serde::Serialize))]
105            $vis enum $name {
106                $( $variant $( { $($field : $ftype),* } )?, )*
107            }
108
109            impl $name {
110                $(
111                    paste::paste! {
112                        pub fn [<$variant:lower>]($($($field : $ftype),*)?) -> Self {
113                            let instance = Self::$variant $( { $($field),* } )?;
114                            // Call the error hook - no need to directly access ERROR_HOOK here
115                            $crate::macros::call_error_hook(
116                                instance.caption(), 
117                                instance.kind(), 
118                                instance.is_fatal(),
119                                instance.is_retryable()
120                            );
121                            instance
122                        }
123                    }
124                )*
125
126                pub fn caption(&self) -> &'static str {
127                    match self {
128                        $( Self::$variant { .. } => {
129                            define_errors!(@get_caption $kind $(, $($tag = $val),* )?)
130                        } ),*
131                    }
132                }
133                
134                pub fn kind(&self) -> &'static str {
135                    match self {
136                        $( Self::$variant { .. } => {
137                            stringify!($kind)
138                        } ),*
139                    }
140                }
141
142                pub fn is_retryable(&self) -> bool {
143                    match self {
144                        $( Self::$variant { .. } => {
145                            define_errors!(@get_tag retryable false $(, $($tag = $val),* )?)
146                        } ),*
147                    }
148                }
149
150                pub fn is_fatal(&self) -> bool {
151                    match self {
152                        $( Self::$variant { .. } => {
153                            define_errors!(@get_tag fatal true $(, $($tag = $val),* )?)
154                        } ),*
155                    }
156                }
157
158                pub fn status_code(&self) -> u16 {
159                    match self {
160                        $( Self::$variant { .. } => {
161                            define_errors!(@get_tag status 500 $(, $($tag = $val),* )?)
162                        } ),*
163                    }
164                }
165
166                pub fn exit_code(&self) -> i32 {
167                    match self {
168                        $( Self::$variant { .. } => {
169                            define_errors!(@get_tag exit 1 $(, $($tag = $val),* )?)
170                        } ),*
171                    }
172                }
173            }
174
175            impl std::fmt::Display for $name {
176                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177                    match self {
178                        $( Self::$variant $( { $($field),* } )? => {
179                            $(                                
180                                #[allow(unused_variables)]
181                                if let Some(display) = define_errors!(@format_display $display $(, $($display_param),*)?) {
182                                    return write!(f, "{}", display);
183                                }
184                            )?
185                            // If no custom display format is provided, use a default format
186                            write!(f, "{}: ", self.caption())?;
187                            write!(f, stringify!($variant))?;
188                            // Format each field with name=value
189                            $( $( 
190                                write!(f, " | {} = ", stringify!($field))?
191                                ;
192                                match stringify!($field) {
193                                    "source" => write!(f, "{}", $field)?,
194                                    _ => write!(f, "{:?}", $field)?,
195                                }
196                            ; )* )?
197                            Ok(())
198                        } ),*
199                    }
200                }
201            }
202
203            impl std::error::Error for $name {
204                fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
205                    match self {
206                        $( Self::$variant $( { $($field),* } )? => {
207                            define_errors!(@find_source $($field),*)
208                        } ),*
209                    }
210                }
211            }
212        )*
213    };
214
215    (@find_source) => {
216        None
217    };
218    
219    (@find_source $field:ident) => {
220        {
221            let val = $field;
222            if let Some(err) = (val as &dyn std::any::Any).downcast_ref::<&(dyn std::error::Error + 'static)>() {
223                Some(*err)
224            } else {
225                None
226            }
227        }
228    };
229    
230    (@find_source $field:ident, $($rest:ident),+) => {
231        {
232            let val = $field;
233            if let Some(err) = (val as &dyn std::any::Any).downcast_ref::<&(dyn std::error::Error + 'static)>() {
234                Some(*err)
235            } else {
236                define_errors!(@find_source $($rest),+)
237            }
238        }
239    };
240
241    (@get_caption $kind:ident $(, caption = $caption:expr $(, $($rest:tt)*)? )?) => {
242        $crate::define_errors!(@unwrap_caption $kind $(, $caption)? )
243    };
244
245    (@unwrap_caption Config, $caption:expr) => { $caption };
246    (@unwrap_caption Filesystem, $caption:expr) => { $caption };
247    (@unwrap_caption $kind:ident) => { stringify!($kind) };
248
249    (@get_tag $target:ident, $default:expr $(, $($tag:ident = $val:expr),* )?) => {
250        {
251            let mut found = $default;
252            $( $( if stringify!($tag) == stringify!($target) { found = $val; })* )?
253            found
254        }
255    };
256    
257    (@format_display $display:literal $(, $($param:ident),*)?) => {
258        {
259            // When parameters are provided, use them for formatting
260            $(
261                Some(format!($display, $($param = $param),*))
262            )?
263            // When no parameters are provided, just use the literal string
264            $(
265                Some($display.to_string())
266            )?
267        }
268    };
269
270    // Support for nested field access in error display formatting
271    (@format_display_field $field:ident) => {
272        $field
273    };
274
275    (@format_display_field $field:ident . $($rest:ident).+) => {
276        $field$(.$rest)+
277    };
278}