explicit_error/
error.rs

1use crate::domain::Domain;
2use crate::fault::*;
3use crate::unwrap_failed;
4use std::{error::Error as StdError, fmt::Display};
5
6/// Use `Result<T, explicit_error::Error>` as the return type of any binary crate
7/// faillible function returning errors.
8/// The [Error::Fault] variant is for errors that should not happen but cannot panic.
9/// The [Error::Domain] variant is for domain errors that provide feedbacks to the user.
10/// For library or functions that require the caller to pattern match on the returned error, a dedicated type is prefered.
11#[derive(Debug)]
12pub enum Error<D> {
13    Domain(Box<D>), // Box for size: https://doc.rust-lang.org/clippy/lint_configuration.html#large-error-threshold
14    Fault(Fault),
15}
16
17impl<D> StdError for Error<D>
18where
19    D: StdError + 'static,
20{
21    fn source(&self) -> Option<&(dyn StdError + 'static)> {
22        match self {
23            Error::Domain(explicit_error) => {
24                explicit_error.source().or(Some(explicit_error.as_ref()))
25            }
26            Error::Fault(fault) => fault.source().or(Some(fault)),
27        }
28    }
29}
30
31impl<D> Display for Error<D>
32where
33    D: StdError,
34{
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            Error::Domain(explicit_error) => Display::fmt(&explicit_error, f),
38            Error::Fault(fault) => fault.fmt(f),
39        }
40    }
41}
42
43impl<D> Error<D>
44where
45    D: StdError + 'static,
46{
47    /// Return true if it's a [Error::Domain] variant
48    pub fn is_domain(&self) -> bool {
49        matches!(*self, Error::Domain(_))
50    }
51
52    /// Return true if it's a [Error::Fault] variant
53    pub fn is_fault(&self) -> bool {
54        !self.is_domain()
55    }
56
57    /// Unwrap the [Error::Domain] variant, panic otherwise
58    pub fn unwrap(self) -> D {
59        match self {
60            Self::Domain(e) => *e,
61            Self::Fault(f) => unwrap_failed("called `Error::unwrap()` on an `Fault` value", &f),
62        }
63    }
64
65    /// Unwrap the [Error::Fault] variant, panic otherwise
66    pub fn unwrap_fault(self) -> Fault {
67        match self {
68            Self::Fault(b) => b,
69            Self::Domain(e) => {
70                unwrap_failed("called `Error::unwrap_err()` on an `Domain` value", &e)
71            }
72        }
73    }
74
75    /// Try to downcast the source of the type wrapped in either [Error::Domain] or [Error::Fault] variant.
76    /// If it is not set try to downcast the type wrapped.
77    /// Usefull to assert_eq! in tests
78    /// # Examples
79    /// ```rust
80    /// use explicit_error_exit::{ExitError, derive::ExitError, Error};
81    /// # use std::process::ExitCode;
82    /// #[test]
83    /// fn test() {
84    ///     assert_eq!(to_test().unwrap_err().downcast_source_ref()::<MyError>().unwrap(), &MyError::Foo);
85    /// }
86    ///
87    /// #[derive(ExitError, Debug)]
88    /// enum MyError {
89    ///     Foo,
90    /// }
91    ///
92    /// # impl From<&MyError> for ExitError {
93    /// #     fn from(value: &MyError) -> Self {
94    /// #         match value {
95    /// #             MyError::Foo => ExitError::new(
96    /// #                     "Something went wrong because ..",
97    /// #                     ExitCode::from(42)
98    /// #                 ),
99    /// #         }
100    /// #     }
101    /// # }
102    ///
103    /// fn to_test() -> Result<(), Error> {
104    ///     Err(MyError::Foo)?;
105    ///     Ok(())
106    /// }
107    /// ```
108    pub fn downcast_source_ref<E>(&self) -> Option<&E>
109    where
110        E: StdError + 'static,
111    {
112        match self {
113            Error::Domain(domain) => match domain.source() {
114                Some(_) => domain.source().unwrap(),
115                None => domain as &dyn StdError,
116            },
117            Error::Fault(fault) => match fault.source() {
118                Some(_) => fault.source().unwrap(),
119                None => fault as &dyn StdError,
120            },
121        }
122        .downcast_ref::<E>()
123    }
124}
125
126impl<D> Error<D>
127where
128    D: Domain,
129{
130    /// Try to downcast the source of the type wrapped in either [Error::Domain] or [Error::Fault] variant.
131    /// If it is not set try to downcast the type wrapped.
132    /// Usefull to assert_eq! in tests
133    /// # Examples
134    /// ```rust
135    /// use explicit_error_exit::{ExitError, derive::ExitError, Error};
136    /// # use std::process::ExitCode;
137    /// #[test]
138    /// fn test() {
139    ///     assert_eq!(to_test().unwrap_err().downcast_source::<MyError>().unwrap(), MyError::Foo);
140    /// }
141    ///
142    /// #[derive(ExitError, Debug)]
143    /// enum MyError {
144    ///     Foo,
145    /// }
146    ///
147    /// # impl From<&MyError> for ExitError {
148    /// #     fn from(value: &MyError) -> Self {
149    /// #         match value {
150    /// #             MyError::Foo => ExitError::new(
151    /// #                     "Something went wrong because ..",
152    /// #                     ExitCode::from(42)
153    /// #                 ),
154    /// #         }
155    /// #     }
156    /// # }
157    ///
158    /// fn to_test() -> Result<(), Error> {
159    ///     Err(MyError::Foo)?;
160    ///     Ok(())
161    /// }
162    /// ```
163    pub fn downcast_source<E>(self) -> Result<E, Box<dyn std::error::Error + 'static + Send + Sync>>
164    where
165        E: StdError + 'static,
166    {
167        match self {
168            Error::Domain(domain) => match domain.source() {
169                Some(_) => domain.into_source().unwrap(),
170                None => domain,
171            },
172            Error::Fault(fault) => match fault.source() {
173                Some(_) => fault.source.unwrap(),
174                None => Box::new(fault),
175            },
176        }
177        .downcast::<E>()
178        .map(|o| *o)
179    }
180
181    /// Add context of either [Error::Domain] or [Error::Fault] variant.
182    /// Override existing context
183    pub fn with_context(self, context: impl Display) -> Self {
184        match self {
185            Error::Domain(d) => Error::Domain(Box::new(d.with_context(context))),
186            Error::Fault(fault) => Error::Fault(fault.with_context(context)),
187        }
188    }
189
190    /// Return the context of either [Error::Domain] or [Error::Fault] variant.
191    pub fn context(&self) -> Option<&str> {
192        match self {
193            Error::Domain(d) => d.context(),
194            Error::Fault(fault) => fault.context(),
195        }
196    }
197}
198
199pub fn errors_chain_debug(source: &dyn StdError) -> String {
200    use std::fmt::Write;
201    let mut source = source;
202    let mut str = format!("{:?}", source);
203
204    while source.source().is_some() {
205        source = source.source().unwrap();
206        let _ = write!(&mut str, "->{:?}", source);
207    }
208
209    str
210}
211
212/// To use this trait on [Result] import the prelude `use explicit_error::prelude::*`
213pub trait ResultFault<T, S> {
214    /// Convert with a closure any error wrapped in a [Result] to an [Error]. Returning an [Ok] convert the wrapped type to
215    /// [Error::Domain].
216    /// Returning an [Err] generates a [Fault] with the orginal error has its source.
217    /// # Examples
218    /// Pattern match to convert to an [Error::Domain]
219    /// ```rust
220    /// # use http::StatusCode;
221    /// # use problem_details::ProblemDetails;
222    /// # use http::Uri;
223    /// # use explicit_error_http::{Error, prelude::*, HttpError, derive::HttpError};
224    /// fn authz_middleware(public_identifier: &str) -> Result<(), Error> {
225    ///     let entity = fetch_bar(&public_identifier).map_err_or_fault(|e|
226    ///         match e {
227    ///             sqlx::Error::RowNotFound => Ok(
228    ///                 NotFoundError::Bar(
229    ///                     public_identifier.to_string())),
230    ///             _ => Err(e), // Convert to Error::Fault
231    ///         }
232    ///     )?;
233    ///
234    ///     Ok(entity)
235    /// }
236    /// # fn fetch_bar(public_identifier: &str) -> Result<(), sqlx::Error> {
237    /// #    Err(sqlx::Error::RowNotFound)
238    /// # }
239    /// # #[derive(HttpError, Debug)]
240    /// # enum NotFoundError {
241    /// #     Bar(String)
242    /// # }
243    /// # impl From<&NotFoundError> for HttpError {
244    /// #   fn from(value: &NotFoundError) -> Self {
245    /// #       let (label, id) = match value {
246    /// #           NotFoundError::Bar(public_identifier) => ("Bar", public_identifier)
247    /// #       };
248    /// #       HttpError::new(
249    /// #           StatusCode::NOT_FOUND,
250    /// #           ProblemDetails::new()
251    /// #               .with_type(Uri::from_static("/errors/not-found"))
252    /// #               .with_title("Not found")
253    /// #               .with_detail(format!("Unknown {label} with identifier {id}."))
254    /// #       )
255    /// #   }
256    /// # }
257    /// ```
258    fn map_err_or_fault<F, E, D>(self, op: F) -> Result<T, Error<D>>
259    where
260        F: FnOnce(S) -> Result<E, S>,
261        E: Into<Error<D>>,
262        S: StdError + 'static + Send + Sync,
263        D: Into<Error<D>>;
264
265    /// Convert any [Result::Err] into a [Result::Err] wrapping a [Fault]
266    /// Use [fault](ResultFault::or_fault) instead if the error implements [std::error::Error]
267    ///  ```rust
268    /// # use std::fs::File;
269    /// # use explicit_error_exit::{Error, prelude::*};
270    /// fn foo() -> Result<(), Error> {
271    ///     let file: Result<File, std::io::Error> = File::open("foo.conf");
272    ///     file.or_fault_no_source().with_context("Configuration file foo.conf is missing.")?;
273    ///
274    ///     Err("error message").or_fault_no_source()?;
275    ///     # Ok(())
276    /// }
277    /// ```
278    fn or_fault_no_source(self) -> Result<T, Fault>;
279
280    /// Convert any [Result::Err] wrapping an error that implements
281    /// [std::error::Error] into a [Result::Err] wrapping a [Fault]
282    ///  ```rust
283    /// # use std::fs::File;
284    /// # use explicit_error_exit::{Error, prelude::*};
285    /// fn foo() -> Result<(), Error> {
286    ///     Err(sqlx::Error::RowNotFound)
287    ///         .or_fault()
288    ///         .with_context("Configuration file foo.conf is missing.")?;
289    ///     # Ok(())
290    /// }
291    /// ```
292    fn or_fault(self) -> Result<T, Fault>
293    where
294        S: StdError + 'static + Send + Sync;
295
296    /// Convert any [Result::Err] into a [Result::Err] wrapping a [Fault] forcing backtrace capture
297    /// Use [or_fault_force](ResultFault::or_fault_force) instead if the error implements [std::error::Error]
298    ///  ```rust
299    /// # use std::fs::File;
300    /// # use explicit_error_exit::{Error, prelude::*};
301    /// fn foo() -> Result<(), Error> {
302    ///     let file: Result<File, std::io::Error> = File::open("foo.conf");
303    ///     file.or_fault_force().with_context("Configuration file foo.conf is missing.")?;
304    ///     # Ok(())
305    /// }
306    /// ```
307    fn or_fault_no_source_force(self) -> Result<T, Fault>;
308
309    /// Convert any [Result::Err] wrapping an error that implements
310    /// [std::error::Error] into a [Result::Err] wrapping a [Fault] forcing backtrace capture
311    ///  ```rust
312    /// # use std::fs::File;
313    /// # use explicit_error_exit::{Error, prelude::*};
314    /// fn foo() -> Result<(), Error> {
315    ///     Err(sqlx::Error::RowNotFound)
316    ///         .or_fault_force()
317    ///         .with_context("Configuration file foo.conf is missing.")?;
318    ///     # Ok(())
319    /// }
320    /// ```
321    fn or_fault_force(self) -> Result<T, Fault>
322    where
323        S: StdError + 'static + Send + Sync;
324}
325
326impl<T, S> ResultFault<T, S> for Result<T, S> {
327    fn map_err_or_fault<F, E, D>(self, op: F) -> Result<T, Error<D>>
328    where
329        F: FnOnce(S) -> Result<E, S>,
330        E: Into<Error<D>>,
331        S: StdError + 'static + Send + Sync,
332        D: Into<Error<D>>,
333    {
334        match self {
335            Ok(ok) => Ok(ok),
336            Err(error) => Err(match op(error) {
337                Ok(d) => d.into(),
338                Err(e) => Fault::new().with_source(e).into(),
339            }),
340        }
341    }
342
343    fn or_fault_no_source(self) -> Result<T, Fault> {
344        match self {
345            Ok(ok) => Ok(ok),
346            Err(_) => Err(Fault::new()),
347        }
348    }
349
350    fn or_fault_no_source_force(self) -> Result<T, Fault> {
351        match self {
352            Ok(ok) => Ok(ok),
353            Err(_) => Err(Fault::new_force()),
354        }
355    }
356
357    fn or_fault(self) -> Result<T, Fault>
358    where
359        S: StdError + 'static + Send + Sync,
360    {
361        match self {
362            Ok(ok) => Ok(ok),
363            Err(error) => Err(Fault::new().with_source(error)),
364        }
365    }
366
367    fn or_fault_force(self) -> Result<T, Fault>
368    where
369        S: StdError + 'static + Send + Sync,
370    {
371        match self {
372            Ok(ok) => Ok(ok),
373            Err(error) => Err(Fault::new_force().with_source(error)),
374        }
375    }
376}
377
378/// To use this trait on [Result] import the prelude `use explicit_error::prelude::*`
379pub trait ResultError<T, D>
380where
381    D: Domain,
382{
383    /// Pattern match on the [Error] source from either the [Error::Fault] or [Error::Domain] variant
384    /// if its type is the closure's parameter type.
385    /// # Examples
386    /// ```rust
387    /// # use http::StatusCode;
388    /// # use http::Uri;
389    /// # use problem_details::ProblemDetails;
390    /// # use explicit_error_http::{prelude::*, HttpError, Result, derive::HttpError};
391    /// # #[derive(HttpError, Debug)]
392    /// # enum MyError {
393    /// #     Foo,
394    /// #     Bar,
395    /// # }
396    /// # impl From<&MyError> for HttpError {
397    /// #    fn from(value: &MyError) -> Self {
398    /// #        match value {
399    /// #            MyError::Foo | MyError::Bar => HttpError::new(
400    /// #                    StatusCode::BAD_REQUEST,
401    /// #                    ProblemDetails::new()
402    /// #                        .with_type(Uri::from_static("/errors/my-domain/foo"))
403    /// #                        .with_title("Foo format incorrect.")
404    /// #                ),
405    /// #        }
406    /// #    }
407    /// # }
408    /// # fn handler() -> Result<()> {
409    ///     let err: Result<()> = Err(MyError::Foo)?;
410    ///     
411    ///     // Do the map if the source's type of the Error is MyError
412    ///     err.try_map_on_source(|e| {
413    ///         match e {
414    ///             MyError::Foo => HttpError::new(
415    ///                 StatusCode::FORBIDDEN,
416    ///                 ProblemDetails::new()
417    ///                     .with_type(Uri::from_static("/errors/forbidden"))
418    ///                ),
419    ///             MyError::Bar => HttpError::new(
420    ///                 StatusCode::UNAUTHORIZED,
421    ///                 ProblemDetails::new()
422    ///                     .with_type(Uri::from_static("/errors/unauthorized"))
423    ///                ),
424    ///         }
425    ///     })?;
426    /// #     Ok(())
427    /// # }
428    /// ```
429    fn try_map_on_source<F, S, E>(self, op: F) -> Result<T, Error<D>>
430    where
431        F: FnOnce(S) -> E,
432        S: StdError + 'static,
433        E: Into<Error<D>>;
434
435    /// Add a context to any variant of an [Error] wrapped in a [Result::Err]
436    /// # Examples
437    /// ```rust
438    /// use explicit_error::{prelude::*, Fault};
439    /// Err::<(), _>(Fault::new()).with_context("Foo bar");
440    /// ```
441    fn with_context(self, context: impl Display) -> Result<T, Error<D>>;
442}
443
444impl<T, D> ResultError<T, D> for Result<T, Error<D>>
445where
446    D: Domain,
447    T: std::fmt::Debug,
448{
449    fn try_map_on_source<F, S, E>(self, op: F) -> Result<T, Error<D>>
450    where
451        F: FnOnce(S) -> E,
452        S: StdError + 'static,
453        E: Into<Error<D>>,
454    {
455        match self {
456            Ok(ok) => Ok(ok),
457            Err(error) => match error {
458                Error::Domain(d) => {
459                    if d.source().is_some() && (d.source().as_ref().unwrap()).is::<S>() {
460                        return Err(op(*d.into_source().unwrap().downcast::<S>().unwrap()).into());
461                    }
462
463                    Err(Error::Domain(d))
464                }
465                Error::Fault(b) => {
466                    if let Some(s) = &b.source {
467                        if s.is::<S>() {
468                            return Err(op(*b.source.unwrap().downcast::<S>().unwrap()).into());
469                        }
470                    }
471
472                    Err(Error::Fault(b))
473                }
474            },
475        }
476    }
477
478    fn with_context(self, context: impl Display) -> Result<T, Error<D>> {
479        match self {
480            Ok(ok) => Ok(ok),
481            Err(error) => Err(match error {
482                Error::Domain(explicit_error) => explicit_error.with_context(context).into(),
483                Error::Fault(fault) => fault.with_context(context).into(),
484            }),
485        }
486    }
487}
488
489/// To use this trait on [Option] import the prelude `use explicit_error::prelude::*`
490pub trait OptionFault<T> {
491    /// Transforms the `Option<T>` into a `Result<T, Fault>`, mapping Some(v) to Ok(v) and None to Err(Fault)
492    /// ```rust
493    /// # use std::fs::File;
494    /// # use explicit_error_exit::{Error, prelude::*};
495    /// fn foo() -> Result<(), Error> {
496    ///     let option: Option<u8> = None;
497    ///     option.ok_or_fault().with_context("Help debugging")?;
498    ///     # Ok(())
499    /// }
500    /// ```
501    fn ok_or_fault(self) -> Result<T, Fault>;
502
503    /// Transforms the `Option<T>` into a `Result<T, Fault>`, mapping Some(v) to Ok(v) and None to Err(Fault)
504    /// forcing backtrace capture
505    /// ```rust
506    /// # use std::fs::File;
507    /// # use explicit_error_exit::{Error, prelude::*};
508    /// fn foo() -> Result<(), Error> {
509    ///     let option: Option<u8> = None;
510    ///     option.ok_or_fault_force().with_context("Help debugging")?;
511    ///     # Ok(())
512    /// }
513    /// ```
514    fn ok_or_fault_force(self) -> Result<T, Fault>;
515}
516
517impl<T> OptionFault<T> for Option<T> {
518    fn ok_or_fault(self) -> Result<T, Fault> {
519        match self {
520            Some(ok) => Ok(ok),
521            None => Err(Fault::new()),
522        }
523    }
524
525    fn ok_or_fault_force(self) -> Result<T, Fault> {
526        match self {
527            Some(ok) => Ok(ok),
528            None => Err(Fault::new_force()),
529        }
530    }
531}
532
533/// To use this trait on [Result] import the prelude `use explicit_error::prelude::*`
534pub trait ResultFaultWithContext<T> {
535    /// Add a context to the [Fault] wrapped in a [Result::Err]
536    /// # Examples
537    /// ```rust
538    /// # use explicit_error::{prelude::*, Fault};
539    /// Err::<(), _>(Fault::new()).with_context("Foo bar");
540    /// ```
541    fn with_context(self, context: impl Display) -> Result<T, Fault>;
542}
543
544impl<T> ResultFaultWithContext<T> for Result<T, Fault> {
545    fn with_context(self, context: impl Display) -> Result<T, Fault> {
546        match self {
547            Ok(ok) => Ok(ok),
548            Err(b) => Err(b.with_context(context)),
549        }
550    }
551}
552
553#[cfg(test)]
554mod test;