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    ///  ```rust
137    /// # use std::fs::File;
138    /// # use explicit_error_exit::{Error, prelude::*};
139    /// fn foo() -> Result<(), Error> {
140    ///     let file: Result<File, std::io::Error> = File::open("foo.conf");
141    ///     file.bug().with_context("Configuration file foo.conf is missing.")?;
142    ///
143    ///     Err("error message").bug()?;
144    ///     # Ok(())
145    /// }
146    /// ```
147    fn bug(self) -> Result<T, Bug>;
148
149    /// Same behavior as `bug` but capture the original error as a source.
150    /// Only applicable to wrapped [std::errror::Error](std::error::Error)
151    fn bug_with_source(self) -> Result<T, Bug>
152    where
153        S: StdError + 'static;
154
155    /// Convert any [Result::Err] into a [Result::Err] wrapping a [Bug] forcing backtrace capture
156    ///  ```rust
157    /// # use std::fs::File;
158    /// # use explicit_error_exit::{Error, prelude::*};
159    /// fn foo() -> Result<(), Error> {
160    ///     let file: Result<File, std::io::Error> = File::open("foo.conf");
161    ///     file.bug_force().with_context("Configuration file foo.conf is missing.")?;
162    ///     # Ok(())
163    /// }
164    /// ```
165    fn bug_force(self) -> Result<T, Bug>;
166
167    /// Same behavior as `bug_force` but capture the original error as a source.
168    /// Only applicable to wrapped [std::errror::Error](std::error::Error)
169    fn bug_force_with_source(self) -> Result<T, Bug>
170    where
171        S: StdError + 'static;
172}
173
174impl<T, S> ResultBug<T, S> for Result<T, S> {
175    fn map_err_or_bug<F, E, D>(self, op: F) -> Result<T, Error<D>>
176    where
177        F: FnOnce(S) -> Result<E, S>,
178        E: Into<Error<D>>,
179        S: StdError + 'static,
180        D: Domain,
181    {
182        match self {
183            Ok(ok) => Ok(ok),
184            Err(error) => Err(match op(error) {
185                Ok(d) => d.into(),
186                Err(e) => Bug::new().with_source(e).into(),
187            }),
188        }
189    }
190
191    fn bug(self) -> Result<T, Bug> {
192        match self {
193            Ok(ok) => Ok(ok),
194            Err(_) => Err(Bug::new()),
195        }
196    }
197
198    fn bug_force(self) -> Result<T, Bug> {
199        match self {
200            Ok(ok) => Ok(ok),
201            Err(_) => Err(Bug::new_force()),
202        }
203    }
204
205    fn bug_with_source(self) -> Result<T, Bug>
206    where
207        S: StdError + 'static,
208    {
209        match self {
210            Ok(ok) => Ok(ok),
211            Err(error) => Err(Bug::new().with_source(error)),
212        }
213    }
214
215    fn bug_force_with_source(self) -> Result<T, Bug>
216    where
217        S: StdError + 'static,
218    {
219        match self {
220            Ok(ok) => Ok(ok),
221            Err(error) => Err(Bug::new_force().with_source(error)),
222        }
223    }
224}
225
226/// To use this trait on [Result] import the prelude `use explicit_error::prelude::*`
227pub trait ResultError<T, D>
228where
229    D: Domain,
230{
231    /// Pattern match on the [Error] source from either the [Error::Bug] or [Error::Domain] variant
232    /// if its type is the closure's parameter type.
233    /// # Examples
234    /// ```rust
235    /// # use actix_web::http::StatusCode;
236    /// # use http::Uri;
237    /// # use problem_details::ProblemDetails;
238    /// # use explicit_error_http::{prelude::*, HttpError, Result, derive::HttpError};
239    /// # #[derive(HttpError, Debug)]
240    /// # enum MyError {
241    /// #     Foo,
242    /// #     Bar,
243    /// # }
244    /// # impl From<&MyError> for HttpError {
245    /// #    fn from(value: &MyError) -> Self {
246    /// #        match value {
247    /// #            MyError::Foo | MyError::Bar => HttpError::new(
248    /// #                    StatusCode::BAD_REQUEST,
249    /// #                    ProblemDetails::new()
250    /// #                        .with_type(Uri::from_static("/errors/my-domain/foo"))
251    /// #                        .with_title("Foo format incorrect.")
252    /// #                ),
253    /// #        }
254    /// #    }
255    /// # }
256    /// # fn handler() -> Result<()> {
257    ///     let err: Result<()> = Err(MyError::Foo)?;
258    ///     
259    ///     // Do the map if the source's type of the Error is MyError
260    ///     err.try_map_on_source(|e| {
261    ///         match e {
262    ///             MyError::Foo => HttpError::new(
263    ///                 StatusCode::FORBIDDEN,
264    ///                 ProblemDetails::new()
265    ///                     .with_type(Uri::from_static("/errors/forbidden"))
266    ///                ),
267    ///             MyError::Bar => HttpError::new(
268    ///                 StatusCode::UNAUTHORIZED,
269    ///                 ProblemDetails::new()
270    ///                     .with_type(Uri::from_static("/errors/unauthorized"))
271    ///                ),
272    ///         }
273    ///     })?;
274    /// #     Ok(())
275    /// # }
276    /// ```
277    fn try_map_on_source<F, S, E>(self, op: F) -> Result<T, Error<D>>
278    where
279        F: FnOnce(S) -> E,
280        S: StdError + 'static,
281        E: Into<Error<D>>;
282
283    /// Add a context to any variant of an [Error] wrapped in a [Result::Err]
284    /// # Examples
285    /// ```rust
286    /// use explicit_error::{prelude::*, Bug};
287    /// Err::<(), _>(Bug::new()).with_context("Foo bar");
288    /// ```
289    fn with_context(self, context: impl Display) -> Result<T, Error<D>>;
290}
291
292impl<T, D> ResultError<T, D> for Result<T, Error<D>>
293where
294    D: Domain,
295{
296    fn try_map_on_source<F, S, E>(self, op: F) -> Result<T, Error<D>>
297    where
298        F: FnOnce(S) -> E,
299        S: StdError + 'static,
300        E: Into<Error<D>>,
301    {
302        match self {
303            Ok(ok) => Ok(ok),
304            Err(error) => match error {
305                Error::Domain(d) => {
306                    if d.source().is_some() && (d.source().as_ref().unwrap()).is::<S>() {
307                        return Err(op(*d.into_source().unwrap().downcast::<S>().unwrap()).into());
308                    }
309
310                    Err(Error::Domain(d))
311                }
312                Error::Bug(b) => {
313                    if let Some(s) = &b.source {
314                        if s.is::<S>() {
315                            return Err(op(*b.source.unwrap().downcast::<S>().unwrap()).into());
316                        }
317                    }
318
319                    Err(Error::Bug(b))
320                }
321            },
322        }
323    }
324
325    fn with_context(self, context: impl Display) -> Result<T, Error<D>> {
326        match self {
327            Ok(ok) => Ok(ok),
328            Err(error) => Err(match error {
329                Error::Domain(explicit_error) => explicit_error.with_context(context).into(),
330                Error::Bug(bug) => bug.with_context(context).into(),
331            }),
332        }
333    }
334}
335
336/// To use this trait on [Option] import the prelude `use explicit_error::prelude::*`
337pub trait OptionBug<T> {
338    /// Convert an [Option::None] into a [Result::Err] wrapping a [Bug]
339    /// ```rust
340    /// # use std::fs::File;
341    /// # use explicit_error_exit::{Error, prelude::*};
342    /// fn foo() -> Result<(), Error> {
343    ///     let option: Option<u8> = None;
344    ///     option.bug().with_context("Help debugging")?;
345    ///     # Ok(())
346    /// }
347    /// ```
348    fn bug(self) -> Result<T, Bug>;
349
350    /// Convert an [Option::None] into a [Result::Err] wrapping a [Bug] forcing backtrace capture
351    /// ```rust
352    /// # use std::fs::File;
353    /// # use explicit_error_exit::{Error, prelude::*};
354    /// fn foo() -> Result<(), Error> {
355    ///     let option: Option<u8> = None;
356    ///     option.bug_force().with_context("Help debugging")?;
357    ///     # Ok(())
358    /// }
359    /// ```
360    fn bug_force(self) -> Result<T, Bug>;
361}
362
363impl<T> OptionBug<T> for Option<T> {
364    fn bug(self) -> Result<T, Bug> {
365        match self {
366            Some(ok) => Ok(ok),
367            None => Err(Bug::new()),
368        }
369    }
370
371    fn bug_force(self) -> Result<T, Bug> {
372        match self {
373            Some(ok) => Ok(ok),
374            None => Err(Bug::new_force()),
375        }
376    }
377}
378
379/// To use this trait on [Result] import the prelude `use explicit_error::prelude::*`
380pub trait ResultBugWithContext<T> {
381    /// Add a context to the [Bug] wrapped in a [Result::Err]
382    /// # Examples
383    /// ```rust
384    /// # use explicit_error::{prelude::*, Bug};
385    /// Err::<(), _>(Bug::new()).with_context("Foo bar");
386    /// ```
387    fn with_context(self, context: impl Display) -> Result<T, Bug>;
388}
389
390impl<T> ResultBugWithContext<T> for Result<T, Bug> {
391    fn with_context(self, context: impl Display) -> Result<T, Bug> {
392        match self {
393            Ok(ok) => Ok(ok),
394            Err(b) => Err(b.with_context(context)),
395        }
396    }
397}