headless_lms_models/
error.rs

1/*!
2Contains error and result types for all the model functions.
3*/
4
5use std::{fmt::Display, num::TryFromIntError};
6
7use backtrace::Backtrace;
8use headless_lms_utils::error::{backend_error::BackendError, util_error::UtilError};
9use tracing_error::SpanTrace;
10use uuid::Uuid;
11
12/**
13Used as the result types for all models.
14
15See also [ModelError] for documentation on how to return errors from models.
16*/
17pub type ModelResult<T> = Result<T, ModelError>;
18
19pub trait TryToOptional<T, E> {
20    fn optional(self) -> Result<Option<T>, E>
21    where
22        Self: Sized;
23}
24
25impl<T> TryToOptional<T, ModelError> for ModelResult<T> {
26    fn optional(self) -> Result<Option<T>, ModelError> {
27        match self {
28            Ok(val) => Ok(Some(val)),
29            Err(err) => {
30                if err.error_type == ModelErrorType::RecordNotFound {
31                    Ok(None)
32                } else {
33                    Err(err)
34                }
35            }
36        }
37    }
38}
39
40/**
41Error type used by all models. Used as the error type in [ModelError], which is used by all the controllers in the application.
42
43All the information in the error is meant to be seen by the user. The type of error is determined by the [ModelErrorType] enum, which is stored inside this struct.
44
45## Examples
46
47### Usage without source error
48
49```no_run
50# use headless_lms_models::prelude::*;
51# fn random_function() -> ModelResult<()> {
52#    let erroneous_condition = 1 == 1;
53if erroneous_condition {
54    return Err(ModelError::new(
55        ModelErrorType::PreconditionFailed,
56        "The user has not enrolled to this course".to_string(),
57        None,
58    ));
59}
60# Ok(())
61# }
62```
63
64### Usage with a source error
65
66Used when calling a function that returns an error that cannot be automatically converted to an ModelError. (See `impl From<X>` implementations on this struct.)
67
68```no_run
69# use headless_lms_models::prelude::*;
70# fn some_function_returning_an_error() -> ModelResult<()> {
71#    return Err(ModelError::new(
72#        ModelErrorType::PreconditionFailed,
73#        "The user has not enrolled to this course".to_string(),
74#        None,
75#    ));
76# }
77#
78# fn random_function() -> ModelResult<()> {
79#    let erroneous_condition = 1 == 1;
80some_function_returning_an_error().map_err(|original_error| {
81    ModelError::new(
82        ModelErrorType::Generic,
83        "Everything went wrong".to_string(),
84        Some(original_error.into()),
85    )
86})?;
87# Ok(())
88# }
89```
90*/
91#[derive(Debug)]
92pub struct ModelError {
93    error_type: ModelErrorType,
94    message: String,
95    /// Original error that caused this error.
96    source: Option<anyhow::Error>,
97    /// A trace of tokio tracing spans, generated automatically when the error is generated.
98    span_trace: Box<SpanTrace>,
99    /// Stack trace, generated automatically when the error is created.
100    backtrace: Box<Backtrace>,
101}
102
103impl std::error::Error for ModelError {
104    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
105        self.source.as_ref().and_then(|o| o.source())
106    }
107
108    fn cause(&self) -> Option<&dyn std::error::Error> {
109        self.source()
110    }
111}
112
113impl Display for ModelError {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(f, "ModelError {:?} {:?}", self.error_type, self.message)
116    }
117}
118
119impl BackendError for ModelError {
120    type ErrorType = ModelErrorType;
121
122    fn new<M: Into<String>, S: Into<Option<anyhow::Error>>>(
123        error_type: Self::ErrorType,
124        message: M,
125        source_error: S,
126    ) -> Self {
127        Self::new_with_traces(
128            error_type,
129            message,
130            source_error,
131            Backtrace::new(),
132            SpanTrace::capture(),
133        )
134    }
135
136    fn backtrace(&self) -> Option<&Backtrace> {
137        Some(&self.backtrace)
138    }
139
140    fn error_type(&self) -> &Self::ErrorType {
141        &self.error_type
142    }
143
144    fn message(&self) -> &str {
145        &self.message
146    }
147
148    fn span_trace(&self) -> &SpanTrace {
149        &self.span_trace
150    }
151
152    fn new_with_traces<M: Into<String>, S: Into<Option<anyhow::Error>>>(
153        error_type: Self::ErrorType,
154        message: M,
155        source_error: S,
156        backtrace: Backtrace,
157        span_trace: SpanTrace,
158    ) -> Self {
159        Self {
160            error_type,
161            message: message.into(),
162            source: source_error.into(),
163            span_trace: Box::new(span_trace),
164            backtrace: Box::new(backtrace),
165        }
166    }
167}
168
169/// The type of [ModelError] that occured.
170#[derive(Debug, PartialEq, Eq)]
171pub enum ModelErrorType {
172    RecordNotFound,
173    NotFound,
174    DatabaseConstraint {
175        constraint: String,
176        description: &'static str,
177    },
178    PreconditionFailed,
179    PreconditionFailedWithCMSAnchorBlockId {
180        id: Uuid,
181        description: &'static str,
182    },
183    InvalidRequest,
184    Conversion,
185    Database,
186    Json,
187    Util,
188    Generic,
189    HttpRequest {
190        status_code: u16,
191        response_body: String,
192    },
193    /// HTTP request failed with specific error details
194    HttpError {
195        error_type: HttpErrorType,
196        reason: String,
197        status_code: Option<u16>,
198        response_body: Option<String>,
199    },
200}
201
202/// Types of HTTP errors that can occur
203#[derive(Debug, PartialEq, Eq)]
204pub enum HttpErrorType {
205    /// HTTP request failed due to network connection issues
206    ConnectionFailed,
207    /// HTTP request failed due to timeout
208    Timeout,
209    /// HTTP request failed due to redirect issues
210    RedirectFailed,
211    /// HTTP request failed due to request building issues
212    RequestBuildFailed,
213    /// HTTP request failed due to response body issues
214    BodyFailed,
215    /// HTTP request succeeded but response body could not be decoded as JSON
216    ResponseDecodeFailed,
217    /// HTTP request failed with non-success status code
218    StatusError,
219    /// Unknown HTTP error type
220    Unknown,
221}
222
223impl From<sqlx::Error> for ModelError {
224    fn from(err: sqlx::Error) -> Self {
225        match &err {
226            sqlx::Error::RowNotFound => ModelError::new(
227                ModelErrorType::RecordNotFound,
228                err.to_string(),
229                Some(err.into()),
230            ),
231            sqlx::Error::Database(db_err) => {
232                if let Some(constraint) = db_err.constraint() {
233                    match constraint {
234                        "email_templates_subject_check" => ModelError::new(
235                            ModelErrorType::DatabaseConstraint {
236                                constraint: constraint.to_string(),
237                                description: "Subject must not be null",
238                            },
239                            err.to_string(),
240                            Some(err.into()),
241                        ),
242                        "user_details_email_check" => ModelError::new(
243                            ModelErrorType::DatabaseConstraint {
244                                constraint: constraint.to_string(),
245                                description: "Email must contain an '@' symbol.",
246                            },
247                            err.to_string(),
248                            Some(err.into()),
249                        ),
250                        _ => ModelError::new(
251                            ModelErrorType::Database,
252                            err.to_string(),
253                            Some(err.into()),
254                        ),
255                    }
256                } else {
257                    ModelError::new(ModelErrorType::Database, err.to_string(), Some(err.into()))
258                }
259            }
260            _ => ModelError::new(ModelErrorType::Database, err.to_string(), Some(err.into())),
261        }
262    }
263}
264
265impl std::convert::From<TryFromIntError> for ModelError {
266    fn from(source: TryFromIntError) -> Self {
267        ModelError::new(
268            ModelErrorType::Conversion,
269            source.to_string(),
270            Some(source.into()),
271        )
272    }
273}
274
275impl std::convert::From<serde_json::Error> for ModelError {
276    fn from(source: serde_json::Error) -> Self {
277        ModelError::new(
278            ModelErrorType::Json,
279            source.to_string(),
280            Some(source.into()),
281        )
282    }
283}
284
285impl std::convert::From<UtilError> for ModelError {
286    fn from(source: UtilError) -> Self {
287        ModelError::new(
288            ModelErrorType::Util,
289            source.to_string(),
290            Some(source.into()),
291        )
292    }
293}
294
295impl From<anyhow::Error> for ModelError {
296    fn from(err: anyhow::Error) -> ModelError {
297        Self::new(ModelErrorType::Conversion, err.to_string(), Some(err))
298    }
299}
300
301impl From<url::ParseError> for ModelError {
302    fn from(err: url::ParseError) -> ModelError {
303        Self::new(ModelErrorType::Generic, err.to_string(), Some(err.into()))
304    }
305}
306
307impl From<reqwest::Error> for ModelError {
308    fn from(err: reqwest::Error) -> Self {
309        let error_type = if err.is_decode() {
310            HttpErrorType::ResponseDecodeFailed
311        } else if err.is_timeout() {
312            HttpErrorType::Timeout
313        } else if err.is_connect() {
314            HttpErrorType::ConnectionFailed
315        } else if err.is_redirect() {
316            HttpErrorType::RedirectFailed
317        } else if err.is_builder() {
318            HttpErrorType::RequestBuildFailed
319        } else if err.is_body() {
320            HttpErrorType::BodyFailed
321        } else if err.is_status() {
322            HttpErrorType::StatusError
323        } else {
324            HttpErrorType::Unknown
325        };
326
327        let status_code = err.status().map(|s| s.as_u16());
328        let response_body = if err.is_decode() {
329            Some("Failed to decode JSON response".to_string())
330        } else {
331            None
332        };
333
334        ModelError::new(
335            ModelErrorType::HttpError {
336                error_type,
337                reason: err.to_string(),
338                status_code,
339                response_body,
340            },
341            format!("HTTP request failed: {}", err),
342            Some(err.into()),
343        )
344    }
345}
346
347#[cfg(test)]
348mod test {
349    use uuid::Uuid;
350
351    use super::*;
352    use crate::{PKeyPolicy, email_templates::EmailTemplateNew, test_helper::*};
353
354    #[tokio::test]
355    async fn email_templates_check() {
356        insert_data!(:tx, :user, :org, :course, :instance);
357
358        let err = crate::email_templates::insert_email_template(
359            tx.as_mut(),
360            instance.id,
361            EmailTemplateNew {
362                name: "".to_string(),
363            },
364            Some(""),
365        )
366        .await
367        .unwrap_err();
368        match err.error_type {
369            ModelErrorType::DatabaseConstraint { constraint, .. } => {
370                assert_eq!(constraint, "email_templates_subject_check");
371            }
372            _ => {
373                panic!("wrong error variant")
374            }
375        }
376    }
377
378    #[tokio::test]
379    async fn user_details_email_check() {
380        let mut conn = Conn::init().await;
381        let mut tx = conn.begin().await;
382        let err = crate::users::insert(
383            tx.as_mut(),
384            PKeyPolicy::Fixed(Uuid::parse_str("92c2d6d6-e1b8-4064-8c60-3ae52266c62c").unwrap()),
385            "invalid email",
386            None,
387            None,
388        )
389        .await
390        .unwrap_err();
391        match err.error_type {
392            ModelErrorType::DatabaseConstraint { constraint, .. } => {
393                assert_eq!(constraint, "user_details_email_check");
394            }
395            _ => {
396                panic!("wrong error variant")
397            }
398        }
399    }
400}