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 {
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 HttpError {
195 error_type: HttpErrorType,
196 reason: String,
197 status_code: Option<u16>,
198 response_body: Option<String>,
199 },
200}
201
202#[derive(Debug, PartialEq, Eq)]
204pub enum HttpErrorType {
205 ConnectionFailed,
207 Timeout,
209 RedirectFailed,
211 RequestBuildFailed,
213 BodyFailed,
215 ResponseDecodeFailed,
217 StatusError,
219 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}