graphql_starter/error/
core.rs

1use std::{collections::HashMap, fmt, sync::Arc};
2
3use error_info::ErrorInfo;
4use http::StatusCode;
5use tracing_error::SpanTrace;
6
7pub type Result<T, E = Box<Error>> = std::result::Result<T, E>;
8
9/// Generic error codes, they're usually not meant for the end-user
10#[derive(Clone, Copy, ErrorInfo)]
11pub enum GenericErrorCode {
12    #[error(status = StatusCode::BAD_REQUEST, message = "The request is not well formed")]
13    BadRequest,
14    #[error(status = StatusCode::UNAUTHORIZED, message = "Not authorized to access this resource")]
15    Unauthorized,
16    #[error(status = StatusCode::FORBIDDEN, message = "Forbidden access to the resource")]
17    Forbidden,
18    #[error(status = StatusCode::NOT_FOUND, message = "The resource could not be found")]
19    NotFound,
20    #[error(status = StatusCode::GATEWAY_TIMEOUT, message = "Timeout exceeded while waiting for a response")]
21    GatewayTimeout,
22    #[error(status = StatusCode::INTERNAL_SERVER_ERROR, message = "Internal server error")]
23    InternalServerError,
24}
25
26/// This type represents an error in the service
27#[derive(Clone)]
28pub struct Error {
29    pub(super) info: Arc<dyn ErrorInfo + Send + Sync + 'static>,
30    pub(super) reason: Option<String>,
31    pub(super) properties: Option<HashMap<String, serde_json::Value>>,
32    pub(super) unexpected: bool,
33    pub(super) source: Option<Arc<dyn fmt::Display + Send + Sync>>,
34    pub(super) context: SpanTrace,
35}
36struct ErrorInfoDebug {
37    status: StatusCode,
38    code: &'static str,
39    raw_message: &'static str,
40    fields: HashMap<String, String>,
41}
42impl fmt::Debug for ErrorInfoDebug {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.debug_struct("ErrorInfo")
45            .field("status", &self.status)
46            .field("code", &self.code)
47            .field("raw_message", &self.raw_message)
48            .field("fields", &self.fields)
49            .finish()
50    }
51}
52impl fmt::Debug for Error {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        f.debug_struct("Error")
55            .field(
56                "info",
57                &ErrorInfoDebug {
58                    status: self.info.status(),
59                    code: self.info.code(),
60                    raw_message: self.info.raw_message(),
61                    fields: self.info.fields(),
62                },
63            )
64            .field("reason", &self.reason)
65            .field("properties", &self.properties)
66            .field("source", &self.source.as_ref().map(|s| s.to_string()))
67            .field("context", &self.context)
68            .finish()
69    }
70}
71impl Error {
72    /// Creates a new [`Box<Error>`](Error), which will be unexpected if the provided info has a server error status
73    pub fn new(info: impl ErrorInfo + Send + Sync + 'static) -> Box<Self> {
74        let info = Arc::new(info);
75        Box::new(Self {
76            unexpected: info.status().is_server_error(),
77            info,
78            reason: None,
79            properties: None,
80            source: None,
81            context: SpanTrace::capture(),
82        })
83    }
84
85    /// Creates a new internal server error
86    pub fn internal(reason: impl Into<String>) -> Box<Self> {
87        Self::new(GenericErrorCode::InternalServerError).with_reason(reason)
88    }
89
90    /// Marks this error as unexpected
91    pub fn unexpected(mut self: Box<Self>) -> Box<Self> {
92        self.unexpected = true;
93        self
94    }
95
96    /// Marks this error as expected
97    pub fn expected(mut self: Box<Self>) -> Box<Self> {
98        self.unexpected = false;
99        self
100    }
101
102    /// Updates the unexpected flag of the error
103    pub fn with_unexpected(mut self: Box<Self>, unexpected: bool) -> Box<Self> {
104        self.unexpected = unexpected;
105        self
106    }
107
108    /// Updates the reason of the error
109    pub fn with_reason(mut self: Box<Self>, reason: impl Into<String>) -> Box<Self> {
110        self.reason = Some(reason.into());
111        self
112    }
113
114    /// Updates the source of the error
115    pub fn with_source<S: fmt::Display + Send + Sync + 'static>(mut self: Box<Self>, source: S) -> Box<Self> {
116        self.source = Some(Arc::new(source));
117        self
118    }
119
120    /// Appends an string property to the error
121    pub fn with_str_property(mut self: Box<Self>, key: &str, value: impl Into<String>) -> Box<Self> {
122        self.properties
123            .get_or_insert_with(HashMap::new)
124            .insert(key.to_string(), serde_json::Value::String(value.into()));
125        self
126    }
127
128    /// Appends a property to the error
129    pub fn with_property(mut self: Box<Self>, key: &str, value: serde_json::Value) -> Box<Self> {
130        self.properties
131            .get_or_insert_with(HashMap::new)
132            .insert(key.to_string(), value);
133        self
134    }
135
136    /// Returns the error info
137    pub fn info(&self) -> &dyn ErrorInfo {
138        self.info.as_ref()
139    }
140
141    /// Returns wether this error is unexpected or not
142    pub fn is_unexpected(&self) -> bool {
143        self.unexpected
144    }
145
146    /// Returns the reason (if any)
147    pub fn reason(&self) -> Option<&str> {
148        self.reason.as_deref()
149    }
150
151    /// Returns the internal properties
152    pub fn properties(&self) -> Option<&HashMap<String, serde_json::Value>> {
153        self.properties.as_ref()
154    }
155
156    /// Returns the reason if any or the default error code message otherwise
157    pub(super) fn reason_or_message(&self) -> String {
158        self.reason.clone().unwrap_or(self.info.message())
159    }
160}
161
162impl fmt::Display for Error {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        let status = self.info.status();
165        write!(
166            f,
167            "[{} {}] {}: {}",
168            status.as_str(),
169            status.canonical_reason().unwrap_or("Unknown"),
170            self.info.code(),
171            self.reason_or_message()
172        )?;
173        if f.alternate() {
174            if let Some(source) = &self.source {
175                write!(f, "\nCaused by: {source}")?;
176            }
177            write!(f, "\n{}", self.context)
178        } else {
179            Ok(())
180        }
181    }
182}
183
184/// Creates a new [`Box<Error>`](Error), which will be unexpected if the provided info has a server error status.
185///
186/// # Examples
187///
188/// ```
189/// # use graphql_starter::{err, error::GenericErrorCode};
190/// # let ctx = "";
191/// # let id = "";
192/// // We can provide a reason
193/// err!("This is the reason for an unexpected internal server error");
194/// err!("This is also, with formatted text: {}", id);
195/// // Or some ErrorInfo
196/// err!(GenericErrorCode::BadRequest);
197/// err!(GenericErrorCode::Forbidden, "Not allowed");
198/// err!(GenericErrorCode::NotFound, "Missing id {}", id);
199/// ````
200#[macro_export]
201macro_rules! err (
202    ($reason:literal) => {
203        $crate::error::Error::internal($reason)
204    };
205    ($reason:literal,) => {
206        $crate::error::Error::internal($reason)
207    };
208    ($reason:literal, $($arg:tt)+) => {
209        $crate::error::Error::internal(format!($reason, $($arg)+))
210    };
211    ($info:expr) => {
212        $crate::error::Error::new($info)
213    };
214    ($info:expr, $reason:literal) => {
215        $crate::error::Error::new($info).with_reason($reason)
216    };
217    ($info:expr, $reason:literal,) => {
218        $crate::error::Error::new($info).with_reason($reason)
219    };
220    ($info:expr, $reason:literal, $($arg:tt)+) => {
221        $crate::error::Error::new($info).with_reason(format!($reason, $($arg)+))
222    };
223);
224pub(crate) use err;
225
226/// Utility trait to map any [`Result<T,E>`](std::result::Result) to a [`Result<T, Box<Error>>`]
227pub trait MapToErr<T> {
228    /// Maps the error to an internal server error
229    fn map_to_internal_err(self, reason: &'static str) -> Result<T>;
230    /// Maps the error to the given one
231    fn map_to_err(self, code: impl ErrorInfo + Send + Sync + 'static) -> Result<T>;
232    /// Maps the error to the given one with a reason
233    fn map_to_err_with(self, code: impl ErrorInfo + Send + Sync + 'static, reason: &'static str) -> Result<T>;
234}
235impl<T, E: fmt::Display + Send + Sync + 'static> MapToErr<T> for Result<T, E> {
236    fn map_to_internal_err(self, reason: &'static str) -> Result<T> {
237        self.map_err(|source| Error::internal(reason).with_source(source))
238    }
239
240    fn map_to_err(self, code: impl ErrorInfo + Send + Sync + 'static) -> Result<T> {
241        self.map_err(|source| Error::new(code).with_source(source))
242    }
243
244    fn map_to_err_with(self, code: impl ErrorInfo + Send + Sync + 'static, reason: &'static str) -> Result<T> {
245        self.map_err(|source| Error::new(code).with_reason(reason).with_source(source))
246    }
247}
248
249/// Utility trait to map any [`Option<T>`] to a [`Result<T, Box<Error>>`]
250pub trait OkOrErr<T> {
251    /// Transforms the option into a [Result], mapping [None] to an internal server error
252    fn ok_or_internal_err(self, reason: &'static str) -> Result<T>;
253    /// Transforms the option into a [Result], mapping [None] to the given error
254    fn ok_or_err(self, code: impl ErrorInfo + Send + Sync + 'static) -> Result<T>;
255    /// Transforms the option into a [Result], mapping [None] to the given error with a reason
256    fn ok_or_err_with(self, code: impl ErrorInfo + Send + Sync + 'static, reason: &'static str) -> Result<T>;
257}
258impl<T> OkOrErr<T> for Option<T> {
259    fn ok_or_internal_err(self, reason: &'static str) -> Result<T> {
260        self.ok_or_else(|| Error::internal(reason))
261    }
262
263    fn ok_or_err(self, code: impl ErrorInfo + Send + Sync + 'static) -> Result<T> {
264        self.ok_or_else(|| Error::new(code))
265    }
266
267    fn ok_or_err_with(self, code: impl ErrorInfo + Send + Sync + 'static, reason: &'static str) -> Result<T> {
268        self.ok_or_else(|| Error::new(code).with_reason(reason))
269    }
270}
271
272/// Utility trait to extend a [Result]
273pub trait ResultExt {
274    /// Marks the error side of the result as unexpected
275    fn unexpected(self) -> Self;
276    /// Marks the error side of the result as expected
277    fn expected(self) -> Self;
278    /// Appends an string property to the error side of the result
279    fn with_str_property(self, key: &'static str, value: &'static str) -> Self;
280}
281impl<T> ResultExt for Result<T> {
282    fn unexpected(self) -> Self {
283        self.map_err(|err| err.unexpected())
284    }
285
286    fn expected(self) -> Self {
287        self.map_err(|err| err.expected())
288    }
289
290    fn with_str_property(self, key: &'static str, value: &'static str) -> Self {
291        self.map_err(|err| err.with_str_property(key, value))
292    }
293}