Skip to main content

headless_lms_utils/error/
util_error.rs

1/*!
2Contains error and result types for all the util functions.
3*/
4
5use std::fmt::Display;
6
7use backtrace::Backtrace;
8use headless_lms_base::error::backend_error::BackendError;
9use tracing_error::SpanTrace;
10
11/**
12Used as the result types for all utils.
13
14See also [UtilError] for documentation on how to return errors from models.
15*/
16pub type UtilResult<T> = Result<T, UtilError>;
17
18/// The type of [UtilError] that occured.
19#[derive(Debug)]
20pub enum UtilErrorType {
21    UrlParse,
22    Walkdir,
23    StripPrefix,
24    TokioIo,
25    SerdeJson,
26    CloudStorage,
27    Other,
28    Unavailable,
29    DeserializationError,
30    TmcHttpError,
31    TmcErrorResponse,
32}
33
34/**
35Error type used by all models. Used as the error type in [UtilError], which is used by all the controllers in the application.
36
37All the information in the error is meant to be seen by the user. The type of error is determined by the [UtilErrorType] enum, which is stored inside this struct.
38
39## Examples
40
41### Usage without source error
42
43```no_run
44# use headless_lms_utils::prelude::*;
45# fn random_function() -> UtilResult<()> {
46#    let erroneous_condition = 1 == 1;
47if erroneous_condition {
48    return Err(UtilError::new(
49        UtilErrorType::Other,
50        "File not found".to_string(),
51        None,
52    ));
53}
54# Ok(())
55# }
56```
57
58### Usage with a source error
59
60Used when calling a function that returns an error that cannot be automatically converted to an UtilError. (See `impl From<X>` implementations on this struct.)
61
62```no_run
63# use headless_lms_utils::prelude::*;
64# fn some_function_returning_an_error() -> UtilResult<()> {
65#    return Err(UtilError::new(
66#        UtilErrorType::Other,
67#        "File not found".to_string(),
68#        None,
69#    ));
70# }
71#
72# fn random_function() -> UtilResult<()> {
73#    let erroneous_condition = 1 == 1;
74some_function_returning_an_error().map_err(|original_error| {
75    UtilError::new(
76        UtilErrorType::Other,
77        "Library x failed to do y".to_string(),
78        Some(original_error.into()),
79    )
80})?;
81# Ok(())
82# }
83```
84*/
85#[derive(Debug)]
86pub struct UtilError {
87    error_type: <UtilError as BackendError>::ErrorType,
88    message: String,
89    /// Original error that caused this error.
90    source: Option<anyhow::Error>,
91    /// A trace of tokio tracing spans, generated automatically when the error is generated.
92    span_trace: Box<SpanTrace>,
93    /// Stack trace, generated automatically when the error is created.
94    backtrace: Box<Backtrace>,
95}
96
97impl std::error::Error for UtilError {
98    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
99        self.source.as_ref().and_then(|o| o.source())
100    }
101
102    fn cause(&self) -> Option<&dyn std::error::Error> {
103        self.source()
104    }
105}
106
107impl Display for UtilError {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        write!(f, "UtilError {:?} {:?}", self.error_type, self.message)
110    }
111}
112
113impl BackendError for UtilError {
114    type ErrorType = UtilErrorType;
115
116    fn new<M: Into<String>, S: Into<Option<anyhow::Error>>>(
117        error_type: Self::ErrorType,
118        message: M,
119        source_error: S,
120    ) -> Self {
121        Self::new_with_traces(
122            error_type,
123            message,
124            source_error,
125            Backtrace::new(),
126            SpanTrace::capture(),
127        )
128    }
129
130    fn backtrace(&self) -> Option<&Backtrace> {
131        Some(&self.backtrace)
132    }
133
134    fn error_type(&self) -> &Self::ErrorType {
135        &self.error_type
136    }
137
138    fn message(&self) -> &str {
139        &self.message
140    }
141
142    fn span_trace(&self) -> &SpanTrace {
143        &self.span_trace
144    }
145
146    fn new_with_traces<M: Into<String>, S: Into<Option<anyhow::Error>>>(
147        error_type: Self::ErrorType,
148        message: M,
149        source_error: S,
150        backtrace: Backtrace,
151        span_trace: SpanTrace,
152    ) -> Self {
153        Self {
154            error_type,
155            message: message.into(),
156            source: source_error.into(),
157            span_trace: Box::new(span_trace),
158            backtrace: Box::new(backtrace),
159        }
160    }
161}
162
163impl From<url::ParseError> for UtilError {
164    fn from(source: url::ParseError) -> Self {
165        UtilError::new(
166            UtilErrorType::UrlParse,
167            source.to_string(),
168            Some(source.into()),
169        )
170    }
171}
172
173impl From<walkdir::Error> for UtilError {
174    fn from(source: walkdir::Error) -> Self {
175        UtilError::new(
176            UtilErrorType::Walkdir,
177            source.to_string(),
178            Some(source.into()),
179        )
180    }
181}
182
183impl From<std::path::StripPrefixError> for UtilError {
184    fn from(source: std::path::StripPrefixError) -> Self {
185        UtilError::new(
186            UtilErrorType::StripPrefix,
187            source.to_string(),
188            Some(source.into()),
189        )
190    }
191}
192
193impl From<tokio::io::Error> for UtilError {
194    fn from(source: tokio::io::Error) -> Self {
195        UtilError::new(
196            UtilErrorType::TokioIo,
197            source.to_string(),
198            Some(source.into()),
199        )
200    }
201}
202
203impl From<serde_json::Error> for UtilError {
204    fn from(source: serde_json::Error) -> Self {
205        UtilError::new(
206            UtilErrorType::SerdeJson,
207            source.to_string(),
208            Some(source.into()),
209        )
210    }
211}
212
213impl From<google_cloud_storage::Error> for UtilError {
214    fn from(source: google_cloud_storage::Error) -> Self {
215        UtilError::new(
216            UtilErrorType::CloudStorage,
217            source.to_string(),
218            Some(source.into()),
219        )
220    }
221}
222
223impl From<anyhow::Error> for UtilError {
224    fn from(err: anyhow::Error) -> UtilError {
225        Self::new(UtilErrorType::Other, err.to_string(), Some(err))
226    }
227}
228
229// Generate error creation macros for UtilError
230crate::define_err_macro!(
231    util_err,
232    UtilError,
233    UtilErrorType,
234    UtilErrorType,
235    "Create a UtilError with less boilerplate."
236);
237
238/// Helper function for `.map_err()` chains to wrap any error as UtilError.
239///
240/// This function creates a closure that converts any error into a `UtilError`
241/// with the specified error type and message, including the original error as the source.
242///
243/// # Examples
244///
245/// ```ignore
246/// // Instead of:
247/// .map_err(|e| UtilError::new(UtilErrorType::Other, e.to_string(), Some(e.into())))?
248///
249/// // You can write:
250/// .map_err(as_util_error(UtilErrorType::Other, "Failed to process".to_string()))?
251/// ```
252pub fn as_util_error<E>(
253    error_type: UtilErrorType,
254    message: impl Into<String>,
255) -> impl FnOnce(E) -> UtilError
256where
257    E: Into<anyhow::Error>,
258{
259    let msg = message.into();
260    move |e| UtilError::new(error_type, msg, Some(e.into()))
261}
262
263/// Helper function for `.ok_or_else()` to create UtilError on None.
264///
265/// This function creates a closure that generates a `UtilError` with the
266/// specified error type and message when called.
267///
268/// # Examples
269///
270/// ```ignore
271/// // Instead of:
272/// .ok_or_else(|| UtilError::new(UtilErrorType::Other, "Item not found".to_string(), None))
273///
274/// // You can write:
275/// .ok_or_else(missing_util_error(UtilErrorType::Other, "Item not found".to_string()))
276/// ```
277pub fn missing_util_error(
278    error_type: UtilErrorType,
279    message: impl Into<String>,
280) -> impl FnOnce() -> UtilError {
281    let msg = message.into();
282    move || UtilError::new(error_type, msg, None)
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_util_err_macro_without_source() {
291        let err = util_err!(Other, "Test error message".to_string());
292        assert_eq!(err.message(), "Test error message");
293        assert!(matches!(err.error_type(), UtilErrorType::Other));
294    }
295
296    #[test]
297    fn test_util_err_macro_with_source() {
298        let source_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
299        let err = util_err!(TokioIo, "Wrapped error".to_string(), source_err);
300        assert_eq!(err.message(), "Wrapped error");
301    }
302
303    #[test]
304    fn test_as_util_error_helper() {
305        let result: Result<(), std::io::Error> = Err(std::io::Error::new(
306            std::io::ErrorKind::NotFound,
307            "test error",
308        ));
309        let util_result = result.map_err(as_util_error(
310            UtilErrorType::TokioIo,
311            "Failed to read file".to_string(),
312        ));
313
314        assert!(util_result.is_err());
315        let err = util_result.unwrap_err();
316        assert_eq!(err.message(), "Failed to read file");
317        assert!(matches!(err.error_type(), UtilErrorType::TokioIo));
318    }
319
320    #[test]
321    fn test_missing_util_error_helper() {
322        let option: Option<String> = None;
323        let result = option.ok_or_else(missing_util_error(
324            UtilErrorType::Other,
325            "Item not found".to_string(),
326        ));
327
328        assert!(result.is_err());
329        let err = result.unwrap_err();
330        assert_eq!(err.message(), "Item not found");
331        assert!(matches!(err.error_type(), UtilErrorType::Other));
332    }
333
334    #[test]
335    fn test_util_err_with_format() {
336        let path = "/tmp/test.txt";
337        let err = util_err!(Other, format!("Failed to process file: {}", path));
338        assert_eq!(err.message(), "Failed to process file: /tmp/test.txt");
339    }
340}