Skip to main content

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#[doc(hidden)]
36pub trait ErrorSource {
37    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)>;
38}
39
40impl ErrorSource for std::io::Error {
41    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
42        Some(self)
43    }
44}
45
46impl ErrorSource for Box<dyn std::error::Error + Send + Sync> {
47    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
48        Some(self.as_ref())
49    }
50}
51
52impl ErrorSource for Box<dyn std::error::Error> {
53    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
54        Some(self.as_ref())
55    }
56}
57
58impl ErrorSource for Option<std::io::Error> {
59    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60        self.as_ref()
61            .map(|error| error as &(dyn std::error::Error + 'static))
62    }
63}
64
65impl ErrorSource for Option<Box<dyn std::error::Error + Send + Sync>> {
66    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
67        self.as_deref()
68            .map(|error| error as &(dyn std::error::Error + 'static))
69    }
70}
71
72impl ErrorSource for Option<Box<dyn std::error::Error>> {
73    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
74        self.as_deref()
75            .map(|error| error as &(dyn std::error::Error + 'static))
76    }
77}
78
79/// Register a callback function to be called when errors are created
80///
81/// # Example
82///
83/// ```rust
84/// use error_forge::{AppError, macros::{register_error_hook, ErrorLevel, ErrorContext}};
85///
86/// // Setup logging with different levels
87/// register_error_hook(|ctx| {
88///     match ctx.level {
89///         ErrorLevel::Debug => println!("DEBUG: {} ({})", ctx.caption, ctx.kind),
90///         ErrorLevel::Info => println!("INFO: {} ({})", ctx.caption, ctx.kind),
91///         ErrorLevel::Warning => println!("WARNING: {} ({})", ctx.caption, ctx.kind),
92///         ErrorLevel::Error => println!("ERROR: {} ({})", ctx.caption, ctx.kind),
93///         ErrorLevel::Critical => println!("CRITICAL: {} ({})", ctx.caption, ctx.kind),
94///     }
95///     
96///     // Optional: send notifications for critical errors
97///     if ctx.level == ErrorLevel::Critical || ctx.is_fatal {
98///         // send_notification("Critical error occurred", ctx.caption);
99///     }
100/// });
101/// ```
102///
103pub fn register_error_hook(callback: fn(ErrorContext)) {
104    let _ = try_register_error_hook(callback);
105}
106
107/// Attempt to register a callback function to be called when errors are created.
108///
109/// Returns an error if a hook is already registered.
110pub fn try_register_error_hook(callback: fn(ErrorContext)) -> Result<(), &'static str> {
111    ERROR_HOOK
112        .set(callback)
113        .map_err(|_| "Error hook already registered")
114}
115
116/// Call the registered error hook with error context if one is registered
117#[doc(hidden)]
118pub fn call_error_hook(caption: &str, kind: &str, is_fatal: bool, is_retryable: bool) {
119    if let Some(hook) = ERROR_HOOK.get() {
120        // Determine error level based on error properties
121        let level = if is_fatal {
122            ErrorLevel::Critical
123        } else if !is_retryable {
124            ErrorLevel::Error
125        } else if kind == "Warning" {
126            ErrorLevel::Warning
127        } else if kind == "Debug" {
128            ErrorLevel::Debug
129        } else {
130            ErrorLevel::Info
131        };
132
133        hook(ErrorContext {
134            caption,
135            kind,
136            level,
137            is_fatal,
138            is_retryable,
139        });
140    }
141}
142
143#[macro_export]
144macro_rules! define_errors {
145    (
146        $(
147            $(#[$meta:meta])* $vis:vis enum $name:ident {
148                $(
149                   $(#[error(display = $display:literal $(, $($display_param:ident),* )?)])?
150                   #[kind($kind:ident $(, $($tag:ident = $val:expr),* )?)]
151                   $variant:ident $( { $($field:ident : $ftype:ty),* $(,)? } )?, )*
152            }
153        )*
154    ) => {
155        $(
156            $(#[$meta])* #[derive(Debug)]
157            #[cfg_attr(feature = "serde", derive(serde::Serialize))]
158            $vis enum $name {
159                $( $variant $( { $($field : $ftype),* } )?, )*
160            }
161
162            impl $name {
163                $(
164                    paste::paste! {
165                        pub fn [<$variant:lower>]($($($field : $ftype),*)?) -> Self {
166                            let instance = Self::$variant $( { $($field),* } )?;
167                            // Call the error hook - no need to directly access ERROR_HOOK here
168                            $crate::macros::call_error_hook(
169                                instance.caption(),
170                                instance.kind(),
171                                instance.is_fatal(),
172                                instance.is_retryable()
173                            );
174                            instance
175                        }
176                    }
177                )*
178
179                pub fn caption(&self) -> &'static str {
180                    match self {
181                        $( Self::$variant { .. } => {
182                            define_errors!(@get_caption $kind $(, $($tag = $val),* )?)
183                        } ),*
184                    }
185                }
186
187                pub fn kind(&self) -> &'static str {
188                    match self {
189                        $( Self::$variant { .. } => {
190                            stringify!($kind)
191                        } ),*
192                    }
193                }
194
195                pub fn is_retryable(&self) -> bool {
196                    match self {
197                        $( Self::$variant { .. } => {
198                            define_errors!(@get_tag retryable, false $(, $($tag = $val),* )?)
199                        } ),*
200                    }
201                }
202
203                pub fn is_fatal(&self) -> bool {
204                    match self {
205                        $( Self::$variant { .. } => {
206                            define_errors!(@get_tag fatal, false $(, $($tag = $val),* )?)
207                        } ),*
208                    }
209                }
210
211                pub fn status_code(&self) -> u16 {
212                    match self {
213                        $( Self::$variant { .. } => {
214                            define_errors!(@get_tag status, 500 $(, $($tag = $val),* )?)
215                        } ),*
216                    }
217                }
218
219                pub fn exit_code(&self) -> i32 {
220                    match self {
221                        $( Self::$variant { .. } => {
222                            define_errors!(@get_tag exit, 1 $(, $($tag = $val),* )?)
223                        } ),*
224                    }
225                }
226            }
227
228            impl std::fmt::Display for $name {
229                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230                    match self {
231                        $( Self::$variant $( { $($field),* } )? => {
232                            $(
233                                #[allow(unused_variables)]
234                                if let Some(display) = define_errors!(@format_display $display $(, $($display_param),*)?) {
235                                    return write!(f, "{}", display);
236                                }
237                            )?
238                            // If no custom display format is provided, use a default format
239                            write!(f, "{}: ", self.caption())?;
240                            write!(f, stringify!($variant))?;
241                            // Format each field with name=value
242                            $( $(
243                                write!(f, " | {} = ", stringify!($field))?
244                                ;
245                                match stringify!($field) {
246                                    "source" => write!(f, "{}", $field)?,
247                                    _ => write!(f, "{:?}", $field)?,
248                                }
249                            ; )* )?
250                            Ok(())
251                        } ),*
252                    }
253                }
254            }
255
256            impl std::error::Error for $name {
257                fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
258                    match self {
259                        $( Self::$variant $( { $($field),* } )? => {
260                            define_errors!(@find_source $( $($field),* )? )
261                        } ),*
262                    }
263                }
264            }
265        )*
266    };
267
268    (@find_source) => {
269        None
270    };
271
272    (@find_source $field:ident $(, $rest:ident)*) => {
273        define_errors!(@find_source_match $field, $field $(, $rest)*)
274    };
275
276    (@find_source_match source, $source_field:ident $(, $rest:ident)*) => {
277        $crate::macros::ErrorSource::as_source($source_field)
278    };
279
280    (@find_source_match $field_name:ident, $field:ident $(, $rest:ident)*) => {
281        define_errors!(@find_source $($rest),*)
282    };
283
284    (@get_caption $kind:ident) => {
285        stringify!($kind)
286    };
287
288    (@get_caption $kind:ident, caption = $caption:expr $(, $($rest:tt)*)?) => {
289        $caption
290    };
291
292    (@get_caption $kind:ident, $tag:ident = $val:expr $(, $($rest:tt)*)?) => {
293        define_errors!(@get_caption $kind $(, $($rest)*)?)
294    };
295
296    (@get_tag $target:ident, $default:expr) => {
297        $default
298    };
299
300    (@get_tag retryable, $default:expr, retryable = $val:expr $(, $($rest:tt)*)?) => {
301        $val
302    };
303
304    (@get_tag fatal, $default:expr, fatal = $val:expr $(, $($rest:tt)*)?) => {
305        $val
306    };
307
308    (@get_tag status, $default:expr, status = $val:expr $(, $($rest:tt)*)?) => {
309        $val
310    };
311
312    (@get_tag exit, $default:expr, exit = $val:expr $(, $($rest:tt)*)?) => {
313        $val
314    };
315
316    (@get_tag $target:ident, $default:expr, $tag:ident = $val:expr $(, $($rest:tt)*)?) => {
317        define_errors!(@get_tag $target, $default $(, $($rest)*)?)
318    };
319
320    (@format_display $display:literal) => {
321        Some($display.to_string())
322    };
323
324    (@format_display $display:literal, $($param:ident),+) => {
325        Some(format!($display, $($param = $param),+))
326    };
327
328    // Support for nested field access in error display formatting
329    (@format_display_field $field:ident) => {
330        $field
331    };
332
333    (@format_display_field $field:ident . $($rest:ident).+) => {
334        $field$(.$rest)+
335    };
336}