explicit_error_http/
domain.rs

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