explicit_error_http/
domain.rs

1use crate::error::HttpError;
2use explicit_error::{Domain, Error};
3use explicit_error_derive::JSONDisplay;
4use serde::{Serialize, Serializer};
5use std::{error::Error as StdError, fmt::Debug};
6
7/// Wrapper for errors that are not a [Bug](explicit_error::Bug). It is used as the [explicit_error::Error::Domain] variant generic type.
8///
9/// It is highly recommended to implement the derive [HttpError](crate::derive::HttpError) which generates the boilerplate
10/// for your domain errors. Otherwise you can implement the [ToDomainError] trait.
11///
12/// [Error](crate::Error) implements `From<DomainError>`, use `?` and `.into()` in functions and closures to convert to the [Error::Domain] variant.
13/// # Examples
14/// [DomainError] can be generated because of a predicate
15/// ```rust
16/// # use actix_web::http::StatusCode;
17/// use explicit_error_http::{HttpError, Result, derive::HttpError};
18///
19/// #[derive(Debug, HttpError)]
20/// enum MyError {
21///     Domain,
22/// }
23///
24/// impl From<&MyError> for HttpError {
25///     fn from(value: &MyError) -> Self {
26///         HttpError::new(
27///             StatusCode::BAD_REQUEST,
28///             "My domain error"
29///         )
30///     }
31/// }
32///
33/// fn business_logic() -> Result<()> {
34///     if 1 < 2 {
35///         Err(MyError::Domain)?;
36///     }
37///     
38///     if true {
39///         Err(HttpError::new(StatusCode::FORBIDDEN, "")
40///             .with_context("Usefull context to debug or monitor"))?;
41///     }
42/// #   Ok(())
43/// }
44/// ```
45/// Or from a [Result]
46/// ```rust
47/// # use actix_web::http::StatusCode;
48/// # use problem_details::ProblemDetails;
49/// # use http::Uri;
50/// use explicit_error_http::{Error, prelude::*, derive::HttpError, HttpError};
51///
52/// #[derive(HttpError, Debug)]
53///  enum NotFoundError {
54///     Bar(String)
55///  }
56///
57///  impl From<&NotFoundError> for HttpError {
58///     fn from(value: &NotFoundError) -> Self {
59///         let (label, id) = match value {
60///             NotFoundError::Bar(public_identifier) => ("Bar", public_identifier)
61///         };
62///
63///         HttpError::new(
64///             StatusCode::NOT_FOUND,
65///             ProblemDetails::new()
66///                 .with_type(Uri::from_static("/errors/not-found"))
67///                 .with_title("Not found")
68///                 .with_detail(format!("Unknown {label} with identifier {id}."))
69///         )
70///     }
71/// }
72///
73/// fn business_logic(public_identifier: &str) -> Result<(), Error> {     
74///     let entity = fetch_bar(&public_identifier).map_err_or_bug(|e|
75///         match e {
76///             sqlx::Error::RowNotFound => Ok(
77///                 NotFoundError::Bar(public_identifier.to_string())),
78///             _ => Err(e),
79///         }
80///     )?;
81///
82///     Ok(entity)
83/// }
84///
85/// fn fetch_bar(public_identifier: &str) -> Result<(), sqlx::Error> {
86///     Err(sqlx::Error::RowNotFound)
87/// }
88/// ```
89/// Or an [Option]
90/// ```rust
91/// # use explicit_error_http::HttpError;
92/// # use actix_web::http::StatusCode;
93/// Some(12).ok_or(HttpError::new(StatusCode::FORBIDDEN, ""))?;
94/// # Ok::<(), HttpError>(())
95/// ```
96#[derive(Debug, Serialize, JSONDisplay)]
97pub struct DomainError {
98    #[serde(flatten)]
99    pub output: HttpError,
100    #[serde(serialize_with = "serialize_source_box")]
101    pub source: Option<Box<dyn StdError>>,
102}
103
104impl Domain for DomainError {
105    fn into_source(self) -> Option<Box<dyn std::error::Error>> {
106        self.source
107    }
108
109    fn with_context(mut self, context: impl std::fmt::Display) -> Self {
110        self.output = self.output.with_context(context);
111        self
112    }
113}
114
115impl From<DomainError> for Error<DomainError> {
116    fn from(value: DomainError) -> Self {
117        Self::Domain(Box::new(value))
118    }
119}
120
121impl StdError for DomainError {
122    fn source(&self) -> Option<&(dyn StdError + 'static)> {
123        self.source.as_ref().map(|o| o.as_ref())
124    }
125}
126
127/// Internally used by [HttpError](crate::derive::HttpError) derive.
128pub trait ToDomainError
129where
130    Self: Sized + StdError + 'static + Into<Error<DomainError>>,
131    for<'a> &'a Self: Into<HttpError>,
132{
133    fn to_domain_error(self) -> DomainError {
134        DomainError {
135            output: (&self).into(),
136            source: Some(Box::new(self)),
137        }
138    }
139
140    fn display(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        #[derive(Serialize)]
142        struct S<'s> {
143            #[serde(flatten)]
144            output: HttpError,
145            #[serde(serialize_with = "serialize_source_dyn")]
146            pub source: &'s dyn StdError,
147        }
148
149        write!(
150            f,
151            "{}",
152            serde_json::json!(S {
153                output: self.into(),
154                source: self,
155            })
156        )
157    }
158}
159
160/// To use this trait on [Result] import the prelude `use explicit_error_http::prelude::*`
161pub trait ResultDomainWithContext<T, D>
162where
163    D: ToDomainError,
164    for<'a> &'a D: Into<HttpError>,
165{
166    /// Add a context to an error that convert to [Error] wrapped in a [Result::Err]
167    /// # Examples
168    /// ```rust
169    /// use explicit_error::{prelude::*, Bug};
170    /// Err::<(), _>(Bug::new()).with_context("Foo bar");
171    /// ```
172    fn with_context(self, context: impl std::fmt::Display) -> std::result::Result<T, DomainError>;
173}
174
175impl<T, D> ResultDomainWithContext<T, D> for Result<T, D>
176where
177    D: ToDomainError,
178    for<'a> &'a D: Into<HttpError>,
179{
180    fn with_context(self, context: impl std::fmt::Display) -> std::result::Result<T, DomainError> {
181        match self {
182            Ok(ok) => Ok(ok),
183            Err(e) => Err(e.to_domain_error().with_context(context)),
184        }
185    }
186}
187
188fn serialize_source_box<S>(source: &Option<Box<dyn StdError>>, s: S) -> Result<S::Ok, S::Error>
189where
190    S: Serializer,
191{
192    if let Some(source) = source {
193        serialize_source_dyn(source.as_ref(), s)
194    } else {
195        s.serialize_none()
196    }
197}
198
199fn serialize_source_dyn<S>(source: &dyn StdError, s: S) -> Result<S::Ok, S::Error>
200where
201    S: Serializer,
202{
203    s.serialize_str(&explicit_error::errors_chain_debug(source))
204}