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}
190
191impl From<sqlx::Error> for ModelError {
192    fn from(err: sqlx::Error) -> Self {
193        match &err {
194            sqlx::Error::RowNotFound => ModelError::new(
195                ModelErrorType::RecordNotFound,
196                err.to_string(),
197                Some(err.into()),
198            ),
199            sqlx::Error::Database(db_err) => {
200                if let Some(constraint) = db_err.constraint() {
201                    match constraint {
202                        "email_templates_subject_check" => ModelError::new(
203                            ModelErrorType::DatabaseConstraint {
204                                constraint: constraint.to_string(),
205                                description: "Subject must not be null",
206                            },
207                            err.to_string(),
208                            Some(err.into()),
209                        ),
210                        "user_details_email_check" => ModelError::new(
211                            ModelErrorType::DatabaseConstraint {
212                                constraint: constraint.to_string(),
213                                description: "Email must contain an '@' symbol.",
214                            },
215                            err.to_string(),
216                            Some(err.into()),
217                        ),
218                        _ => ModelError::new(
219                            ModelErrorType::Database,
220                            err.to_string(),
221                            Some(err.into()),
222                        ),
223                    }
224                } else {
225                    ModelError::new(ModelErrorType::Database, err.to_string(), Some(err.into()))
226                }
227            }
228            _ => ModelError::new(ModelErrorType::Database, err.to_string(), Some(err.into())),
229        }
230    }
231}
232
233impl std::convert::From<TryFromIntError> for ModelError {
234    fn from(source: TryFromIntError) -> Self {
235        ModelError::new(
236            ModelErrorType::Conversion,
237            source.to_string(),
238            Some(source.into()),
239        )
240    }
241}
242
243impl std::convert::From<serde_json::Error> for ModelError {
244    fn from(source: serde_json::Error) -> Self {
245        ModelError::new(
246            ModelErrorType::Json,
247            source.to_string(),
248            Some(source.into()),
249        )
250    }
251}
252
253impl std::convert::From<UtilError> for ModelError {
254    fn from(source: UtilError) -> Self {
255        ModelError::new(
256            ModelErrorType::Util,
257            source.to_string(),
258            Some(source.into()),
259        )
260    }
261}
262
263impl From<anyhow::Error> for ModelError {
264    fn from(err: anyhow::Error) -> ModelError {
265        Self::new(ModelErrorType::Conversion, err.to_string(), Some(err))
266    }
267}
268
269impl From<url::ParseError> for ModelError {
270    fn from(err: url::ParseError) -> ModelError {
271        Self::new(ModelErrorType::Generic, err.to_string(), Some(err.into()))
272    }
273}
274
275#[cfg(test)]
276mod test {
277    use uuid::Uuid;
278
279    use super::*;
280    use crate::{PKeyPolicy, email_templates::EmailTemplateNew, test_helper::*};
281
282    #[tokio::test]
283    async fn email_templates_check() {
284        insert_data!(:tx, :user, :org, :course, :instance);
285
286        let err = crate::email_templates::insert_email_template(
287            tx.as_mut(),
288            instance.id,
289            EmailTemplateNew {
290                name: "".to_string(),
291            },
292            Some(""),
293        )
294        .await
295        .unwrap_err();
296        match err.error_type {
297            ModelErrorType::DatabaseConstraint { constraint, .. } => {
298                assert_eq!(constraint, "email_templates_subject_check");
299            }
300            _ => {
301                panic!("wrong error variant")
302            }
303        }
304    }
305
306    #[tokio::test]
307    async fn user_details_email_check() {
308        let mut conn = Conn::init().await;
309        let mut tx = conn.begin().await;
310        let err = crate::users::insert(
311            tx.as_mut(),
312            PKeyPolicy::Fixed(Uuid::parse_str("92c2d6d6-e1b8-4064-8c60-3ae52266c62c").unwrap()),
313            "invalid email",
314            None,
315            None,
316        )
317        .await
318        .unwrap_err();
319        match err.error_type {
320            ModelErrorType::DatabaseConstraint { constraint, .. } => {
321                assert_eq!(constraint, "user_details_email_check");
322            }
323            _ => {
324                panic!("wrong error variant")
325            }
326        }
327    }
328}