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    /// matched in From<sqlx::Error> for ModelError to get the constraint that was violated
175    DatabaseConstraint {
176        constraint: String,
177        description: &'static str,
178    },
179    PreconditionFailed,
180    PreconditionFailedWithCMSAnchorBlockId {
181        id: Uuid,
182        description: &'static str,
183    },
184    InvalidRequest,
185    Conversion,
186    Database,
187    Json,
188    Util,
189    Generic,
190    HttpRequest {
191        status_code: u16,
192        response_body: String,
193    },
194    /// HTTP request failed with specific error details
195    HttpError {
196        error_type: HttpErrorType,
197        reason: String,
198        status_code: Option<u16>,
199        response_body: Option<String>,
200    },
201}
202
203/// Types of HTTP errors that can occur
204#[derive(Debug, PartialEq, Eq)]
205pub enum HttpErrorType {
206    /// HTTP request failed due to network connection issues
207    ConnectionFailed,
208    /// HTTP request failed due to timeout
209    Timeout,
210    /// HTTP request failed due to redirect issues
211    RedirectFailed,
212    /// HTTP request failed due to request building issues
213    RequestBuildFailed,
214    /// HTTP request failed due to response body issues
215    BodyFailed,
216    /// HTTP request succeeded but response body could not be decoded as JSON
217    ResponseDecodeFailed,
218    /// HTTP request failed with non-success status code
219    StatusError,
220    /// Unknown HTTP error type
221    Unknown,
222}
223
224impl From<sqlx::Error> for ModelError {
225    fn from(err: sqlx::Error) -> Self {
226        match &err {
227            sqlx::Error::RowNotFound => ModelError::new(
228                ModelErrorType::RecordNotFound,
229                err.to_string(),
230                Some(err.into()),
231            ),
232            sqlx::Error::Database(db_err) => {
233                if let Some(constraint) = db_err.constraint() {
234                    match constraint {
235                        "email_templates_subject_check" => ModelError::new(
236                            ModelErrorType::DatabaseConstraint {
237                                constraint: constraint.to_string(),
238                                description: "Subject must not be null",
239                            },
240                            err.to_string(),
241                            Some(err.into()),
242                        ),
243                        "user_details_email_check" => ModelError::new(
244                            ModelErrorType::DatabaseConstraint {
245                                constraint: constraint.to_string(),
246                                description: "Email must contain an '@' symbol.",
247                            },
248                            err.to_string(),
249                            Some(err.into()),
250                        ),
251                        "unique_chatbot_names_within_course" => ModelError::new(
252                            ModelErrorType::DatabaseConstraint {
253                                constraint: constraint.to_string(),
254                                description: "The chatbot name is already taken by another chatbot on this course",
255                            },
256                            err.to_string(),
257                            Some(err.into()),
258                        ),
259                        _ => ModelError::new(
260                            ModelErrorType::Database,
261                            err.to_string(),
262                            Some(err.into()),
263                        ),
264                    }
265                } else {
266                    ModelError::new(ModelErrorType::Database, err.to_string(), Some(err.into()))
267                }
268            }
269            _ => ModelError::new(ModelErrorType::Database, err.to_string(), Some(err.into())),
270        }
271    }
272}
273
274impl std::convert::From<TryFromIntError> for ModelError {
275    fn from(source: TryFromIntError) -> Self {
276        ModelError::new(
277            ModelErrorType::Conversion,
278            source.to_string(),
279            Some(source.into()),
280        )
281    }
282}
283
284impl std::convert::From<serde_json::Error> for ModelError {
285    fn from(source: serde_json::Error) -> Self {
286        ModelError::new(
287            ModelErrorType::Json,
288            source.to_string(),
289            Some(source.into()),
290        )
291    }
292}
293
294impl std::convert::From<UtilError> for ModelError {
295    fn from(source: UtilError) -> Self {
296        ModelError::new(
297            ModelErrorType::Util,
298            source.to_string(),
299            Some(source.into()),
300        )
301    }
302}
303
304impl From<anyhow::Error> for ModelError {
305    fn from(err: anyhow::Error) -> ModelError {
306        Self::new(ModelErrorType::Conversion, err.to_string(), Some(err))
307    }
308}
309
310impl From<url::ParseError> for ModelError {
311    fn from(err: url::ParseError) -> ModelError {
312        Self::new(ModelErrorType::Generic, err.to_string(), Some(err.into()))
313    }
314}
315
316impl From<reqwest::Error> for ModelError {
317    fn from(err: reqwest::Error) -> Self {
318        let error_type = if err.is_decode() {
319            HttpErrorType::ResponseDecodeFailed
320        } else if err.is_timeout() {
321            HttpErrorType::Timeout
322        } else if err.is_connect() {
323            HttpErrorType::ConnectionFailed
324        } else if err.is_redirect() {
325            HttpErrorType::RedirectFailed
326        } else if err.is_builder() {
327            HttpErrorType::RequestBuildFailed
328        } else if err.is_body() {
329            HttpErrorType::BodyFailed
330        } else if err.is_status() {
331            HttpErrorType::StatusError
332        } else {
333            HttpErrorType::Unknown
334        };
335
336        let status_code = err.status().map(|s| s.as_u16());
337        let response_body = if err.is_decode() {
338            Some("Failed to decode JSON response".to_string())
339        } else {
340            None
341        };
342
343        ModelError::new(
344            ModelErrorType::HttpError {
345                error_type,
346                reason: err.to_string(),
347                status_code,
348                response_body,
349            },
350            format!("HTTP request failed: {}", err),
351            Some(err.into()),
352        )
353    }
354}
355
356#[cfg(test)]
357mod test {
358    use uuid::Uuid;
359
360    use super::*;
361    use crate::{
362        PKeyPolicy,
363        email_templates::{EmailTemplateNew, EmailTemplateType},
364        test_helper::*,
365    };
366
367    #[tokio::test]
368    async fn email_templates_check() {
369        insert_data!(:tx, :user, :org, :course);
370
371        let err = crate::email_templates::insert_email_template(
372            tx.as_mut(),
373            Some(course),
374            EmailTemplateNew {
375                template_type: EmailTemplateType::Generic,
376                language: None,
377                content: None,
378                subject: None,
379            },
380            Some(""),
381        )
382        .await
383        .unwrap_err();
384        match err.error_type {
385            ModelErrorType::DatabaseConstraint { constraint, .. } => {
386                assert_eq!(constraint, "email_templates_subject_check");
387            }
388            _ => {
389                panic!("wrong error variant")
390            }
391        }
392    }
393
394    #[tokio::test]
395    async fn user_details_email_check() {
396        let mut conn = Conn::init().await;
397        let mut tx = conn.begin().await;
398        let err = crate::users::insert(
399            tx.as_mut(),
400            PKeyPolicy::Fixed(Uuid::parse_str("92c2d6d6-e1b8-4064-8c60-3ae52266c62c").unwrap()),
401            "invalid email",
402            None,
403            None,
404        )
405        .await
406        .unwrap_err();
407        match err.error_type {
408            ModelErrorType::DatabaseConstraint { constraint, .. } => {
409                assert_eq!(constraint, "user_details_email_check");
410            }
411            _ => {
412                panic!("wrong error variant")
413            }
414        }
415    }
416}