error_forge/
macros.rs

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