Skip to main content

headless_lms_credit_registration/
error.rs

1//! The error of a phase iteration that could not do its job. What a Suotar answer does to a row, a
2//! row another writer moved on, or a breaker holding a phase back are values, not errors.
3
4use std::fmt::Display;
5use std::panic::Location;
6
7use backtrace::Backtrace;
8use headless_lms_base::error::backend_error::BackendError;
9use headless_lms_models::ModelError;
10use headless_lms_utils::error::is_db_disconnect;
11use headless_lms_utils::error::util_error::UtilError;
12use tracing_error::SpanTrace;
13
14/// The result of anything that can fail with a [`CreditRegistrationError`].
15pub type CreditRegistrationResult<T> = Result<T, CreditRegistrationError>;
16
17/// The type of [`CreditRegistrationError`] that occurred.
18#[derive(Debug, PartialEq, Eq)]
19pub enum CreditRegistrationErrorType {
20    /// A models call failed. The [`ModelError`] is the source; see
21    /// [`CreditRegistrationError::into_model_error`].
22    Model,
23    /// The pool, or a transaction's begin or commit.
24    Database,
25}
26
27/// Why a credit registration phase iteration, or a controller calling into one, could not finish.
28/// A database failure mid-iteration is one; nothing Suotar answers is. For a models failure,
29/// [`Self::into_model_error`] recovers the [`ModelError`].
30pub struct CreditRegistrationError {
31    error_type: <CreditRegistrationError as BackendError>::ErrorType,
32    message: String,
33    source: Option<anyhow::Error>,
34    span_trace: Box<SpanTrace>,
35    backtrace: Box<Backtrace>,
36    location: Option<&'static Location<'static>>,
37}
38
39impl std::error::Error for CreditRegistrationError {
40    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
41        self.source
42            .as_deref()
43            .map(|e| e as &(dyn std::error::Error + 'static))
44    }
45
46    fn cause(&self) -> Option<&dyn std::error::Error> {
47        self.source()
48    }
49}
50
51headless_lms_base::impl_clean_debug!(
52    CreditRegistrationError,
53    [CreditRegistrationError, ModelError, UtilError]
54);
55
56impl Display for CreditRegistrationError {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(
59            f,
60            "CreditRegistrationError {:?} {:?}",
61            self.error_type, self.message
62        )
63    }
64}
65
66impl BackendError for CreditRegistrationError {
67    type ErrorType = CreditRegistrationErrorType;
68
69    fn backtrace(&self) -> Option<&Backtrace> {
70        Some(&self.backtrace)
71    }
72
73    fn error_type(&self) -> &Self::ErrorType {
74        &self.error_type
75    }
76
77    fn message(&self) -> &str {
78        &self.message
79    }
80
81    fn span_trace(&self) -> &SpanTrace {
82        &self.span_trace
83    }
84
85    fn location(&self) -> Option<&'static Location<'static>> {
86        self.location
87    }
88
89    fn new_with_traces_and_location<M: Into<String>, S: Into<Option<anyhow::Error>>>(
90        error_type: Self::ErrorType,
91        message: M,
92        source_error: S,
93        backtrace: Backtrace,
94        span_trace: SpanTrace,
95        location: Option<&'static Location<'static>>,
96    ) -> Self {
97        Self {
98            error_type,
99            message: message.into(),
100            source: source_error.into(),
101            span_trace: Box::new(span_trace),
102            backtrace: Box::new(backtrace),
103            location,
104        }
105    }
106}
107
108impl CreditRegistrationError {
109    /// Unwraps a [`CreditRegistrationErrorType::Model`] error into its [`ModelError`], so a
110    /// controller maps it as it maps any other, and a missing course still answers 404. Any other
111    /// error is handed back unchanged.
112    pub fn into_model_error(mut self) -> Result<ModelError, Self> {
113        if self.error_type != CreditRegistrationErrorType::Model {
114            return Err(self);
115        }
116        match self.source.take().map(|source| source.downcast()) {
117            Some(Ok(model_error)) => Ok(model_error),
118            Some(Err(source)) => {
119                self.source = Some(source);
120                Err(self)
121            }
122            None => Err(self),
123        }
124    }
125
126    /// The message and every cause under it, joined with `: `, for a plain-text record such as a
127    /// phase's last error. A cause whose text is already in the chain is left out, since each
128    /// wrapper's message repeats the error it wraps.
129    pub fn cause_chain(&self) -> String {
130        let mut chain = self.message.clone();
131        let mut cause = std::error::Error::source(self);
132        while let Some(error) = cause {
133            let text = error.to_string();
134            if !chain.contains(&text) {
135                chain.push_str(": ");
136                chain.push_str(&text);
137            }
138            cause = error.source();
139        }
140        chain
141    }
142
143    /// Whether the database connection was lost anywhere in the cause chain; see
144    /// [`is_db_disconnect`].
145    pub fn is_db_disconnect(&self) -> bool {
146        is_db_disconnect(self)
147    }
148}
149
150impl From<ModelError> for CreditRegistrationError {
151    #[track_caller]
152    fn from(err: ModelError) -> Self {
153        Self::new(
154            CreditRegistrationErrorType::Model,
155            err.to_string(),
156            Some(err.into()),
157        )
158    }
159}
160
161impl From<sqlx::Error> for CreditRegistrationError {
162    #[track_caller]
163    fn from(err: sqlx::Error) -> Self {
164        Self::new(
165            CreditRegistrationErrorType::Database,
166            err.to_string(),
167            Some(err.into()),
168        )
169    }
170}