explicit_error/
error.rs

1use crate::bug::*;
2use crate::domain::Domain;
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::Bug] 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: Domain> {
13    Domain(Box<D>), // Box for size: https://doc.rust-lang.org/clippy/lint_configuration.html#large-error-threshold
14    Bug(Bug),
15}
16
17impl<D> StdError for Error<D>
18where
19    D: Domain,
20{
21    fn source(&self) -> Option<&(dyn StdError + 'static)> {
22        match self {
23            Error::Domain(explicit_error) => Some(explicit_error.as_ref()),
24            Error::Bug(bug) => bug.source.as_ref().map(|e| e.as_ref()),
25        }
26    }
27}
28
29impl<D> Display for Error<D>
30where
31    D: Domain,
32{
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Error::Domain(explicit_error) => Display::fmt(&explicit_error, f),
36            Error::Bug(bug) => bug.fmt(f),
37        }
38    }
39}
40
41impl<D> Error<D>
42where
43    D: Domain,
44{
45    pub fn is_domain(&self) -> bool {
46        matches!(*self, Error::Domain(_))
47    }
48
49    pub fn is_bug(&self) -> bool {
50        !self.is_domain()
51    }
52    pub fn unwrap(self) -> D {
53        match self {
54            Self::Domain(e) => *e,
55            Self::Bug(b) => unwrap_failed("called `Error::unwrap()` on an `Bug` value", &b),
56        }
57    }
58
59    pub fn unwrap_bug(self) -> Bug {
60        match self {
61            Self::Bug(b) => b,
62            Self::Domain(e) => {
63                unwrap_failed("called `Error::unwrap_err()` on an `Domain` value", &e)
64            }
65        }
66    }
67}
68
69pub fn errors_chain_debug(source: &dyn StdError) -> String {
70    use std::fmt::Write;
71    let mut source = source;
72    let mut str = format!("{:?}", source);
73
74    while source.source().is_some() {
75        source = source.source().unwrap();
76        let _ = write!(&mut str, "->{:?}", source);
77    }
78
79    str
80}
81
82/// To use this trait on [Result] import the prelude `use explicit_error::prelude::*`
83pub trait ResultBug<T, S> {
84    /// Convert with a closure any error wrapped in a [Result] to an [Error]. Returning an [Ok] convert the wrapped type to
85    /// [Error::Domain].
86    /// Returning an [Err] generates a [Bug] with the orginal error has its source.
87    /// # Examples
88    /// Pattern match to convert to an [Error::Domain]
89    /// ```rust
90    /// # use actix_web::http::StatusCode;
91    /// # use problem_details::ProblemDetails;
92    /// # use http::Uri;
93    /// # use explicit_error_http::{Error, prelude::*, HttpError, derive::HttpError};
94    /// fn authz_middleware(public_identifier: &str) -> Result<(), Error> {
95    ///     let entity = fetch_bar(&public_identifier).map_err_or_bug(|e|
96    ///         match e {
97    ///             sqlx::Error::RowNotFound => Ok(
98    ///                 NotFoundError::Bar(
99    ///                     public_identifier.to_string())),
100    ///             _ => Err(e), // Convert to Error::Bug
101    ///         }
102    ///     )?;
103    ///
104    ///     Ok(entity)
105    /// }
106    /// # fn fetch_bar(public_identifier: &str) -> Result<(), sqlx::Error> {
107    /// #    Err(sqlx::Error::RowNotFound)
108    /// # }
109    /// # #[derive(HttpError, Debug)]
110    /// # enum NotFoundError {
111    /// #     Bar(String)
112    /// # }
113    /// # impl From<&NotFoundError> for HttpError {
114    /// #   fn from(value: &NotFoundError) -> Self {
115    /// #       let (label, id) = match value {
116    /// #           NotFoundError::Bar(public_identifier) => ("Bar", public_identifier)
117    /// #       };
118    /// #       HttpError::new(
119    /// #           StatusCode::NOT_FOUND,
120    /// #           ProblemDetails::new()
121    /// #               .with_type(Uri::from_static("/errors/not-found"))
122    /// #               .with_title("Not found")
123    /// #               .with_detail(format!("Unknown {label} with identifier {id}."))
124    /// #       )
125    /// #   }
126    /// # }
127    /// ```
128    fn map_err_or_bug<F, E, D>(self, op: F) -> Result<T, Error<D>>
129    where
130        F: FnOnce(S) -> Result<E, S>,
131        E: Into<Error<D>>,
132        S: StdError + 'static,
133        D: Domain;
134
135    /// Convert any [Result::Err] into a [Result::Err] wrapping a [Bug]
136    /// Use [bug](ResultBug::bug) instead if the error implements [std::error::Error]
137    ///  ```rust
138    /// # use std::fs::File;
139    /// # use explicit_error_exit::{Error, prelude::*};
140    /// fn foo() -> Result<(), Error> {
141    ///     let file: Result<File, std::io::Error> = File::open("foo.conf");
142    ///     file.bug_no_source().with_context("Configuration file foo.conf is missing.")?;
143    ///
144    ///     Err("error message").bug_no_source()?;
145    ///     # Ok(())
146    /// }
147    /// ```
148    fn bug_no_source(self) -> Result<T, Bug>;
149
150    /// Convert any [Result::Err] wrapping an error that implements
151    /// [std::error::Error] into a [Result::Err] wrapping a [Bug]
152    ///  ```rust
153    /// # use std::fs::File;
154    /// # use explicit_error_exit::{Error, prelude::*};
155    /// fn foo() -> Result<(), Error> {
156    ///     Err(sqlx::Error::RowNotFound)
157    ///         .bug()
158    ///         .with_context("Configuration file foo.conf is missing.")?;
159    ///     # Ok(())
160    /// }
161    /// ```
162    fn bug(self) -> Result<T, Bug>
163    where
164        S: StdError + 'static;
165
166    /// Convert any [Result::Err] into a [Result::Err] wrapping a [Bug] forcing backtrace capture
167    /// Use [bug_force](ResultBug::bug_force) instead if the error implements [std::error::Error]
168    ///  ```rust
169    /// # use std::fs::File;
170    /// # use explicit_error_exit::{Error, prelude::*};
171    /// fn foo() -> Result<(), Error> {
172    ///     let file: Result<File, std::io::Error> = File::open("foo.conf");
173    ///     file.bug_force().with_context("Configuration file foo.conf is missing.")?;
174    ///     # Ok(())
175    /// }
176    /// ```
177    fn bug_no_source_force(self) -> Result<T, Bug>;
178
179    /// Convert any [Result::Err] wrapping an error that implements
180    /// [std::error::Error] into a [Result::Err] wrapping a [Bug] forcing backtrace capture
181    ///  ```rust
182    /// # use std::fs::File;
183    /// # use explicit_error_exit::{Error, prelude::*};
184    /// fn foo() -> Result<(), Error> {
185    ///     Err(sqlx::Error::RowNotFound)
186    ///         .bug_force()
187    ///         .with_context("Configuration file foo.conf is missing.")?;
188    ///     # Ok(())
189    /// }
190    /// ```
191    fn bug_force(self) -> Result<T, Bug>
192    where
193        S: StdError + 'static;
194}
195
196impl<T, S> ResultBug<T, S> for Result<T, S> {
197    fn map_err_or_bug<F, E, D>(self, op: F) -> Result<T, Error<D>>
198    where
199        F: FnOnce(S) -> Result<E, S>,
200        E: Into<Error<D>>,
201        S: StdError + 'static,
202        D: Domain,
203    {
204        match self {
205            Ok(ok) => Ok(ok),
206            Err(error) => Err(match op(error) {
207                Ok(d) => d.into(),
208                Err(e) => Bug::new().with_source(e).into(),
209            }),
210        }
211    }
212
213    fn bug_no_source(self) -> Result<T, Bug> {
214        match self {
215            Ok(ok) => Ok(ok),
216            Err(_) => Err(Bug::new()),
217        }
218    }
219
220    fn bug_no_source_force(self) -> Result<T, Bug> {
221        match self {
222            Ok(ok) => Ok(ok),
223            Err(_) => Err(Bug::new_force()),
224        }
225    }
226
227    fn bug(self) -> Result<T, Bug>
228    where
229        S: StdError + 'static,
230    {
231        match self {
232            Ok(ok) => Ok(ok),
233            Err(error) => Err(Bug::new().with_source(error)),
234        }
235    }
236
237    fn bug_force(self) -> Result<T, Bug>
238    where
239        S: StdError + 'static,
240    {
241        match self {
242            Ok(ok) => Ok(ok),
243            Err(error) => Err(Bug::new_force().with_source(error)),
244        }
245    }
246}
247
248/// To use this trait on [Result] import the prelude `use explicit_error::prelude::*`
249pub trait ResultError<T, D>
250where
251    D: Domain,
252{
253    /// Pattern match on the [Error] source from either the [Error::Bug] or [Error::Domain] variant
254    /// if its type is the closure's parameter type.
255    /// # Examples
256    /// ```rust
257    /// # use actix_web::http::StatusCode;
258    /// # use http::Uri;
259    /// # use problem_details::ProblemDetails;
260    /// # use explicit_error_http::{prelude::*, HttpError, Result, derive::HttpError};
261    /// # #[derive(HttpError, Debug)]
262    /// # enum MyError {
263    /// #     Foo,
264    /// #     Bar,
265    /// # }
266    /// # impl From<&MyError> for HttpError {
267    /// #    fn from(value: &MyError) -> Self {
268    /// #        match value {
269    /// #            MyError::Foo | MyError::Bar => HttpError::new(
270    /// #                    StatusCode::BAD_REQUEST,
271    /// #                    ProblemDetails::new()
272    /// #                        .with_type(Uri::from_static("/errors/my-domain/foo"))
273    /// #                        .with_title("Foo format incorrect.")
274    /// #                ),
275    /// #        }
276    /// #    }
277    /// # }
278    /// # fn handler() -> Result<()> {
279    ///     let err: Result<()> = Err(MyError::Foo)?;
280    ///     
281    ///     // Do the map if the source's type of the Error is MyError
282    ///     err.try_map_on_source(|e| {
283    ///         match e {
284    ///             MyError::Foo => HttpError::new(
285    ///                 StatusCode::FORBIDDEN,
286    ///                 ProblemDetails::new()
287    ///                     .with_type(Uri::from_static("/errors/forbidden"))
288    ///                ),
289    ///             MyError::Bar => HttpError::new(
290    ///                 StatusCode::UNAUTHORIZED,
291    ///                 ProblemDetails::new()
292    ///                     .with_type(Uri::from_static("/errors/unauthorized"))
293    ///                ),
294    ///         }
295    ///     })?;
296    /// #     Ok(())
297    /// # }
298    /// ```
299    fn try_map_on_source<F, S, E>(self, op: F) -> Result<T, Error<D>>
300    where
301        F: FnOnce(S) -> E,
302        S: StdError + 'static,
303        E: Into<Error<D>>;
304
305    /// Add a context to any variant of an [Error] wrapped in a [Result::Err]
306    /// # Examples
307    /// ```rust
308    /// use explicit_error::{prelude::*, Bug};
309    /// Err::<(), _>(Bug::new()).with_context("Foo bar");
310    /// ```
311    fn with_context(self, context: impl Display) -> Result<T, Error<D>>;
312}
313
314impl<T, D> ResultError<T, D> for Result<T, Error<D>>
315where
316    D: Domain,
317{
318    fn try_map_on_source<F, S, E>(self, op: F) -> Result<T, Error<D>>
319    where
320        F: FnOnce(S) -> E,
321        S: StdError + 'static,
322        E: Into<Error<D>>,
323    {
324        match self {
325            Ok(ok) => Ok(ok),
326            Err(error) => match error {
327                Error::Domain(d) => {
328                    if d.source().is_some() && (d.source().as_ref().unwrap()).is::<S>() {
329                        return Err(op(*d.into_source().unwrap().downcast::<S>().unwrap()).into());
330                    }
331
332                    Err(Error::Domain(d))
333                }
334                Error::Bug(b) => {
335                    if let Some(s) = &b.source {
336                        if s.is::<S>() {
337                            return Err(op(*b.source.unwrap().downcast::<S>().unwrap()).into());
338                        }
339                    }
340
341                    Err(Error::Bug(b))
342                }
343            },
344        }
345    }
346
347    fn with_context(self, context: impl Display) -> Result<T, Error<D>> {
348        match self {
349            Ok(ok) => Ok(ok),
350            Err(error) => Err(match error {
351                Error::Domain(explicit_error) => explicit_error.with_context(context).into(),
352                Error::Bug(bug) => bug.with_context(context).into(),
353            }),
354        }
355    }
356}
357
358/// To use this trait on [Option] import the prelude `use explicit_error::prelude::*`
359pub trait OptionBug<T> {
360    /// Convert an [Option::None] into a [Result::Err] wrapping a [Bug]
361    /// ```rust
362    /// # use std::fs::File;
363    /// # use explicit_error_exit::{Error, prelude::*};
364    /// fn foo() -> Result<(), Error> {
365    ///     let option: Option<u8> = None;
366    ///     option.bug().with_context("Help debugging")?;
367    ///     # Ok(())
368    /// }
369    /// ```
370    fn bug(self) -> Result<T, Bug>;
371
372    /// Convert an [Option::None] into a [Result::Err] wrapping a [Bug] forcing backtrace capture
373    /// ```rust
374    /// # use std::fs::File;
375    /// # use explicit_error_exit::{Error, prelude::*};
376    /// fn foo() -> Result<(), Error> {
377    ///     let option: Option<u8> = None;
378    ///     option.bug_force().with_context("Help debugging")?;
379    ///     # Ok(())
380    /// }
381    /// ```
382    fn bug_force(self) -> Result<T, Bug>;
383}
384
385impl<T> OptionBug<T> for Option<T> {
386    fn bug(self) -> Result<T, Bug> {
387        match self {
388            Some(ok) => Ok(ok),
389            None => Err(Bug::new()),
390        }
391    }
392
393    fn bug_force(self) -> Result<T, Bug> {
394        match self {
395            Some(ok) => Ok(ok),
396            None => Err(Bug::new_force()),
397        }
398    }
399}
400
401/// To use this trait on [Result] import the prelude `use explicit_error::prelude::*`
402pub trait ResultBugWithContext<T> {
403    /// Add a context to the [Bug] wrapped in a [Result::Err]
404    /// # Examples
405    /// ```rust
406    /// # use explicit_error::{prelude::*, Bug};
407    /// Err::<(), _>(Bug::new()).with_context("Foo bar");
408    /// ```
409    fn with_context(self, context: impl Display) -> Result<T, Bug>;
410}
411
412impl<T> ResultBugWithContext<T> for Result<T, Bug> {
413    fn with_context(self, context: impl Display) -> Result<T, Bug> {
414        match self {
415            Ok(ok) => Ok(ok),
416            Err(b) => Err(b.with_context(context)),
417        }
418    }
419}