1use 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
12pub 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#[derive(Debug)]
92pub struct ModelError {
93 error_type: ModelErrorType,
94 message: String,
95 source: Option<anyhow::Error>,
97 span_trace: Box<SpanTrace>,
99 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#[derive(Debug, PartialEq, Eq)]
171pub enum ModelErrorType {
172 RecordNotFound,
173 NotFound,
174 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 HttpError {
196 error_type: HttpErrorType,
197 reason: String,
198 status_code: Option<u16>,
199 response_body: Option<String>,
200 },
201}
202
203#[derive(Debug, PartialEq, Eq)]
205pub enum HttpErrorType {
206 ConnectionFailed,
208 Timeout,
210 RedirectFailed,
212 RequestBuildFailed,
214 BodyFailed,
216 ResponseDecodeFailed,
218 StatusError,
220 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}