Skip to main content

error_forge/
macros.rs

1/// Error severity level passed to a registered hook callback.
2///
3/// Marked `#[non_exhaustive]` so future minor releases can add new
4/// severity variants (e.g. `Notice`, `Trace`) without breaking
5/// existing `match` statements.
6#[derive(Clone, Copy, PartialEq, Eq, Debug)]
7#[non_exhaustive]
8pub enum ErrorLevel {
9    /// Debug-level errors (for detailed debugging)
10    Debug,
11    /// Information-level errors (least severe)
12    Info,
13    /// Warning-level errors (moderate severity)
14    Warning,
15    /// Error-level errors (high severity)
16    Error,
17    /// Critical-level errors (most severe)
18    Critical,
19}
20
21/// Error context passed to registered hooks.
22///
23/// Marked `#[non_exhaustive]` so future minor releases can add new
24/// fields without breaking callers that destructure the struct.
25/// Construct via [`ErrorContext::new`] (rather than struct-literal
26/// syntax) from outside the crate.
27#[non_exhaustive]
28pub struct ErrorContext<'a> {
29    /// The error caption
30    pub caption: &'a str,
31    /// The error kind
32    pub kind: &'a str,
33    /// The error level
34    pub level: ErrorLevel,
35    /// Whether the error is fatal
36    pub is_fatal: bool,
37    /// Whether the error can be retried
38    pub is_retryable: bool,
39}
40
41impl<'a> ErrorContext<'a> {
42    /// Construct an [`ErrorContext`] from its components.
43    ///
44    /// Provided so external callers (tests, custom hook wiring) can
45    /// build the struct without depending on its field list, which
46    /// may grow over the `1.x` line.
47    pub fn new(
48        caption: &'a str,
49        kind: &'a str,
50        level: ErrorLevel,
51        is_fatal: bool,
52        is_retryable: bool,
53    ) -> Self {
54        Self {
55            caption,
56            kind,
57            level,
58            is_fatal,
59            is_retryable,
60        }
61    }
62}
63
64use std::sync::OnceLock;
65
66/// Hook callback type.
67///
68/// Stored as a boxed `Fn` so callers can capture environment in a
69/// closure (a `Write`-implementing buffer, a thread-safe logger
70/// handle, an `Arc<Config>`, etc.). The `Send + Sync` bounds let
71/// the hook fire from any thread.
72type ErrorHookFn = Box<dyn Fn(ErrorContext<'_>) + Send + Sync + 'static>;
73
74/// Global error hook for centralized error handling.
75static ERROR_HOOK: OnceLock<ErrorHookFn> = OnceLock::new();
76
77#[doc(hidden)]
78pub trait ErrorSource {
79    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)>;
80}
81
82impl ErrorSource for std::io::Error {
83    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
84        Some(self)
85    }
86}
87
88impl ErrorSource for Box<dyn std::error::Error + Send + Sync> {
89    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
90        Some(self.as_ref())
91    }
92}
93
94impl ErrorSource for Box<dyn std::error::Error> {
95    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
96        Some(self.as_ref())
97    }
98}
99
100impl ErrorSource for Option<std::io::Error> {
101    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
102        self.as_ref()
103            .map(|error| error as &(dyn std::error::Error + 'static))
104    }
105}
106
107impl ErrorSource for Option<Box<dyn std::error::Error + Send + Sync>> {
108    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
109        self.as_deref()
110            .map(|error| error as &(dyn std::error::Error + 'static))
111    }
112}
113
114impl ErrorSource for Option<Box<dyn std::error::Error>> {
115    fn as_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
116        self.as_deref()
117            .map(|error| error as &(dyn std::error::Error + 'static))
118    }
119}
120
121/// Register a callback to be called when errors are created.
122///
123/// **Deprecated since `1.0.0`.** This variant silently discards
124/// the registration failure when a hook is already installed.
125/// Use [`try_register_error_hook`] instead — it returns the
126/// failure explicitly so callers can decide how to handle the
127/// double-registration case.
128///
129/// # Example
130///
131/// ```
132/// use error_forge::macros::{try_register_error_hook, ErrorLevel};
133///
134/// let _ = try_register_error_hook(|ctx| {
135///     match ctx.level {
136///         ErrorLevel::Debug => println!("DEBUG: {} ({})", ctx.caption, ctx.kind),
137///         ErrorLevel::Info => println!("INFO: {} ({})", ctx.caption, ctx.kind),
138///         ErrorLevel::Warning => println!("WARNING: {} ({})", ctx.caption, ctx.kind),
139///         ErrorLevel::Error => println!("ERROR: {} ({})", ctx.caption, ctx.kind),
140///         ErrorLevel::Critical => println!("CRITICAL: {} ({})", ctx.caption, ctx.kind),
141///         // `ErrorLevel` is `#[non_exhaustive]` — minor releases
142///         // may add new severity levels.
143///         _ => println!("OTHER: {} ({})", ctx.caption, ctx.kind),
144///     }
145/// });
146/// ```
147#[deprecated(
148    since = "1.0.0",
149    note = "register_error_hook silently drops registration failures; use \
150            try_register_error_hook instead"
151)]
152pub fn register_error_hook<F>(callback: F)
153where
154    F: Fn(ErrorContext<'_>) + Send + Sync + 'static,
155{
156    let _ = try_register_error_hook(callback);
157}
158
159/// Attempt to register a callback to be called when errors are
160/// created.
161///
162/// The callback may be a function pointer or a closure capturing
163/// thread-safe state. Only one hook can be registered per process;
164/// subsequent calls return `Err("Error hook already registered")`.
165///
166/// # Example
167///
168/// ```
169/// use error_forge::macros::try_register_error_hook;
170/// use std::sync::{Arc, Mutex};
171///
172/// // Closures that capture state work too — not just function pointers.
173/// let log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
174/// let log_for_hook = Arc::clone(&log);
175/// let _ = try_register_error_hook(move |ctx| {
176///     log_for_hook
177///         .lock()
178///         .unwrap()
179///         .push(format!("{}: {}", ctx.kind, ctx.caption));
180/// });
181/// ```
182pub fn try_register_error_hook<F>(callback: F) -> Result<(), &'static str>
183where
184    F: Fn(ErrorContext<'_>) + Send + Sync + 'static,
185{
186    ERROR_HOOK
187        .set(Box::new(callback))
188        .map_err(|_| "Error hook already registered")
189}
190
191/// Call the registered error hook with error context if one is registered
192#[doc(hidden)]
193pub fn call_error_hook(caption: &str, kind: &str, is_fatal: bool, is_retryable: bool) {
194    if let Some(hook) = ERROR_HOOK.get() {
195        // Determine error level based on error properties
196        let level = if is_fatal {
197            ErrorLevel::Critical
198        } else if !is_retryable {
199            ErrorLevel::Error
200        } else if kind == "Warning" {
201            ErrorLevel::Warning
202        } else if kind == "Debug" {
203            ErrorLevel::Debug
204        } else {
205            ErrorLevel::Info
206        };
207
208        hook(ErrorContext {
209            caption,
210            kind,
211            level,
212            is_fatal,
213            is_retryable,
214        });
215    }
216}
217
218#[macro_export]
219macro_rules! define_errors {
220    (
221        $(
222            $(#[$meta:meta])* $vis:vis enum $name:ident {
223                $(
224                   $(#[error(display = $display:literal $(, $($display_param:ident),* )?)])?
225                   #[kind($kind:ident $(, $($tag:ident = $val:expr),* )?)]
226                   $variant:ident $( { $($field:ident : $ftype:ty),* $(,)? } )?, )*
227            }
228        )*
229    ) => {
230        $(
231            $(#[$meta])* #[derive(Debug)]
232            #[cfg_attr(feature = "serde", derive(serde::Serialize))]
233            $vis enum $name {
234                $( $variant $( { $($field : $ftype),* } )?, )*
235            }
236
237            impl $name {
238                $(
239                    $crate::__private::pastey::paste! {
240                        pub fn [<$variant:lower>]($($($field : $ftype),*)?) -> Self {
241                            let instance = Self::$variant $( { $($field),* } )?;
242                            // Call the error hook - no need to directly access ERROR_HOOK here
243                            $crate::macros::call_error_hook(
244                                instance.caption(),
245                                instance.kind(),
246                                instance.is_fatal(),
247                                instance.is_retryable()
248                            );
249                            instance
250                        }
251                    }
252                )*
253
254                pub fn caption(&self) -> &'static str {
255                    match self {
256                        $( Self::$variant { .. } => {
257                            define_errors!(@get_caption $kind $(, $($tag = $val),* )?)
258                        } ),*
259                    }
260                }
261
262                pub fn kind(&self) -> &'static str {
263                    match self {
264                        $( Self::$variant { .. } => {
265                            stringify!($kind)
266                        } ),*
267                    }
268                }
269
270                pub fn is_retryable(&self) -> bool {
271                    match self {
272                        $( Self::$variant { .. } => {
273                            define_errors!(@get_tag retryable, false $(, $($tag = $val),* )?)
274                        } ),*
275                    }
276                }
277
278                pub fn is_fatal(&self) -> bool {
279                    match self {
280                        $( Self::$variant { .. } => {
281                            define_errors!(@get_tag fatal, false $(, $($tag = $val),* )?)
282                        } ),*
283                    }
284                }
285
286                pub fn status_code(&self) -> u16 {
287                    match self {
288                        $( Self::$variant { .. } => {
289                            define_errors!(@get_tag status, 500 $(, $($tag = $val),* )?)
290                        } ),*
291                    }
292                }
293
294                pub fn exit_code(&self) -> i32 {
295                    match self {
296                        $( Self::$variant { .. } => {
297                            define_errors!(@get_tag exit, 1 $(, $($tag = $val),* )?)
298                        } ),*
299                    }
300                }
301            }
302
303            impl std::fmt::Display for $name {
304                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305                    match self {
306                        $( Self::$variant $( { $($field),* } )? => {
307                            $(
308                                #[allow(unused_variables)]
309                                if let Some(display) = define_errors!(@format_display $display $(, $($display_param),*)?) {
310                                    return write!(f, "{}", display);
311                                }
312                            )?
313                            // If no custom display format is provided, use a default format
314                            write!(f, "{}: ", self.caption())?;
315                            write!(f, stringify!($variant))?;
316                            // Format each field with name=value
317                            $( $(
318                                write!(f, " | {} = ", stringify!($field))?
319                                ;
320                                match stringify!($field) {
321                                    "source" => write!(f, "{}", $field)?,
322                                    _ => write!(f, "{:?}", $field)?,
323                                }
324                            ; )* )?
325                            Ok(())
326                        } ),*
327                    }
328                }
329            }
330
331            impl std::error::Error for $name {
332                fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
333                    match self {
334                        $( Self::$variant $( { $($field),* } )? => {
335                            define_errors!(@find_source $( $($field),* )? )
336                        } ),*
337                    }
338                }
339            }
340        )*
341    };
342
343    (@find_source) => {
344        None
345    };
346
347    (@find_source $field:ident $(, $rest:ident)*) => {
348        define_errors!(@find_source_match $field, $field $(, $rest)*)
349    };
350
351    (@find_source_match source, $source_field:ident $(, $rest:ident)*) => {
352        $crate::macros::ErrorSource::as_source($source_field)
353    };
354
355    (@find_source_match $field_name:ident, $field:ident $(, $rest:ident)*) => {
356        define_errors!(@find_source $($rest),*)
357    };
358
359    (@get_caption $kind:ident) => {
360        stringify!($kind)
361    };
362
363    (@get_caption $kind:ident, caption = $caption:expr $(, $($rest:tt)*)?) => {
364        $caption
365    };
366
367    (@get_caption $kind:ident, $tag:ident = $val:expr $(, $($rest:tt)*)?) => {
368        define_errors!(@get_caption $kind $(, $($rest)*)?)
369    };
370
371    (@get_tag $target:ident, $default:expr) => {
372        $default
373    };
374
375    (@get_tag retryable, $default:expr, retryable = $val:expr $(, $($rest:tt)*)?) => {
376        $val
377    };
378
379    (@get_tag fatal, $default:expr, fatal = $val:expr $(, $($rest:tt)*)?) => {
380        $val
381    };
382
383    (@get_tag status, $default:expr, status = $val:expr $(, $($rest:tt)*)?) => {
384        $val
385    };
386
387    (@get_tag exit, $default:expr, exit = $val:expr $(, $($rest:tt)*)?) => {
388        $val
389    };
390
391    (@get_tag $target:ident, $default:expr, $tag:ident = $val:expr $(, $($rest:tt)*)?) => {
392        define_errors!(@get_tag $target, $default $(, $($rest)*)?)
393    };
394
395    (@format_display $display:literal) => {
396        Some($display.to_string())
397    };
398
399    (@format_display $display:literal, $($param:ident),+) => {
400        Some(format!($display, $($param = $param),+))
401    };
402
403    // Support for nested field access in error display formatting
404    (@format_display_field $field:ident) => {
405        $field
406    };
407
408    (@format_display_field $field:ident . $($rest:ident).+) => {
409        $field$(.$rest)+
410    };
411}