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