1#[derive(Clone, Copy, PartialEq, Eq, Debug)]
7#[non_exhaustive]
8pub enum ErrorLevel {
9 Debug,
11 Info,
13 Warning,
15 Error,
17 Critical,
19}
20
21#[non_exhaustive]
28pub struct ErrorContext<'a> {
29 pub caption: &'a str,
31 pub kind: &'a str,
33 pub level: ErrorLevel,
35 pub is_fatal: bool,
37 pub is_retryable: bool,
39}
40
41impl<'a> ErrorContext<'a> {
42 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
66type ErrorHookFn = Box<dyn Fn(ErrorContext<'_>) + Send + Sync + 'static>;
73
74static 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#[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
159pub 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#[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 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 $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 write!(f, "{}: ", self.caption())?;
315 write!(f, stringify!($variant))?;
316 $( $(
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 (@format_display_field $field:ident) => {
405 $field
406 };
407
408 (@format_display_field $field:ident . $($rest:ident).+) => {
409 $field$(.$rest)+
410 };
411}