1#[derive(Clone, Copy, PartialEq, Eq, Debug)]
3pub enum ErrorLevel {
4 Debug,
6 Info,
8 Warning,
10 Error,
12 Critical,
14}
15
16pub struct ErrorContext<'a> {
18 pub caption: &'a str,
20 pub kind: &'a str,
22 pub level: ErrorLevel,
24 pub is_fatal: bool,
26 pub is_retryable: bool,
28}
29
30use std::sync::OnceLock;
31
32static 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
79pub fn register_error_hook(callback: fn(ErrorContext)) {
104 let _ = try_register_error_hook(callback);
105}
106
107pub 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#[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 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 $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 write!(f, "{}: ", self.caption())?;
240 write!(f, stringify!($variant))?;
241 $( $(
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 (@format_display_field $field:ident) => {
330 $field
331 };
332
333 (@format_display_field $field:ident . $($rest:ident).+) => {
334 $field$(.$rest)+
335 };
336}