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 [Bug](explicit_error::Bug). 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_bug(|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 with_context(mut self, context: impl std::fmt::Display) -> Self {
109 self.output = self.output.with_context(context);
110 self
111 }
112}
113
114impl From<DomainError> for Error<DomainError> {
115 fn from(value: DomainError) -> Self {
116 Self::Domain(Box::new(value))
117 }
118}
119
120impl StdError for DomainError {
121 fn source(&self) -> Option<&(dyn StdError + 'static)> {
122 self.source.as_ref().map(|o| o.as_ref())
123 }
124}
125
126#[derive(Serialize)]
127struct DomainErrorDisplay<'s> {
128 #[serde(flatten)]
129 output: HttpErrorDisplay<'s>,
130 #[serde(serialize_with = "serialize_option_source_dyn")]
131 pub source: Option<&'s dyn StdError>,
132}
133
134impl<'s> From<&'s DomainError> for DomainErrorDisplay<'s> {
135 fn from(value: &'s DomainError) -> Self {
136 Self {
137 output: HttpErrorDisplay::<'s>::from(&value.output),
138 source: value.source.as_deref(),
139 }
140 }
141}
142
143impl std::fmt::Display for DomainError {
144 fn fmt<'s>(&'s self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 write!(
146 f,
147 "{}",
148 serde_json::json!(DomainErrorDisplay::<'s>::from(self))
149 )
150 }
151}
152
153/// Internally used by [HttpError](crate::derive::HttpError) derive.
154pub trait ToDomainError
155where
156 Self: Sized + StdError + 'static + Into<Error<DomainError>>,
157 for<'a> &'a Self: Into<HttpError>,
158{
159 fn to_domain_error(self) -> DomainError {
160 DomainError {
161 output: (&self).into(),
162 source: Some(Box::new(self)),
163 }
164 }
165
166 fn display(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 #[derive(Serialize)]
168 struct S<'s> {
169 #[serde(flatten)]
170 output: HttpError,
171 #[serde(serialize_with = "serialize_source_dyn")]
172 pub source: &'s dyn StdError,
173 }
174
175 write!(
176 f,
177 r#"{{"output":"{}","source":}}{}"#,
178 Into::<HttpError>::into(self),
179 serde_json::json!(S {
180 output: self.into(),
181 source: self,
182 })
183 )
184 }
185}
186
187/// To use this trait on [Result] import the prelude `use explicit_error_http::prelude::*`
188pub trait ResultDomainWithContext<T, D>
189where
190 D: ToDomainError,
191 for<'a> &'a D: Into<HttpError>,
192{
193 /// Add a context to an error that convert to [Error] wrapped in a [Result::Err]
194 /// # Examples
195 /// ```rust
196 /// use explicit_error::{prelude::*, Bug};
197 /// Err::<(), _>(Bug::new()).with_context("Foo bar");
198 /// ```
199 fn with_context(self, context: impl std::fmt::Display) -> std::result::Result<T, DomainError>;
200}
201
202impl<T, D> ResultDomainWithContext<T, D> for Result<T, D>
203where
204 D: ToDomainError,
205 for<'a> &'a D: Into<HttpError>,
206{
207 fn with_context(self, context: impl std::fmt::Display) -> std::result::Result<T, DomainError> {
208 match self {
209 Ok(ok) => Ok(ok),
210 Err(e) => Err(e.to_domain_error().with_context(context)),
211 }
212 }
213}
214
215fn serialize_source_dyn<S>(source: &dyn StdError, s: S) -> Result<S::Ok, S::Error>
216where
217 S: Serializer,
218{
219 s.serialize_str(&explicit_error::errors_chain_debug(source))
220}
221
222fn serialize_option_source_dyn<S>(source: &Option<&dyn StdError>, s: S) -> Result<S::Ok, S::Error>
223where
224 S: Serializer,
225{
226 match source {
227 Some(source) => serialize_source_dyn(source, s),
228 None => s.serialize_none(),
229 }
230}
231
232#[cfg(test)]
233mod test;