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}
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}