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}