celers_core/
error.rs

1use thiserror::Error;
2
3pub type Result<T> = std::result::Result<T, CelersError>;
4
5#[derive(Error, Debug)]
6pub enum CelersError {
7    #[error("Serialization error: {0}")]
8    Serialization(String),
9
10    #[error("Deserialization error: {0}")]
11    Deserialization(String),
12
13    #[error("Broker error: {0}")]
14    Broker(String),
15
16    #[error("Task not found: {0}")]
17    TaskNotFound(String),
18
19    #[error("Task execution failed: {0}")]
20    TaskExecution(String),
21
22    #[error("Task was revoked: {0}")]
23    TaskRevoked(crate::TaskId),
24
25    #[error("Task timeout: {0}")]
26    Timeout(String),
27
28    #[error("Invalid task state transition from {from:?} to {to:?}")]
29    InvalidStateTransition { from: String, to: String },
30
31    #[error("Configuration error: {0}")]
32    Configuration(String),
33
34    #[error("IO error: {0}")]
35    Io(#[from] std::io::Error),
36
37    #[error("Other error: {0}")]
38    Other(String),
39}
40
41impl CelersError {
42    /// Check if the error is serialization-related
43    #[inline]
44    #[must_use]
45    pub const fn is_serialization(&self) -> bool {
46        matches!(self, CelersError::Serialization(_))
47    }
48
49    /// Check if the error is deserialization-related
50    #[inline]
51    #[must_use]
52    pub const fn is_deserialization(&self) -> bool {
53        matches!(self, CelersError::Deserialization(_))
54    }
55
56    /// Check if the error is broker-related
57    #[inline]
58    #[must_use]
59    pub const fn is_broker(&self) -> bool {
60        matches!(self, CelersError::Broker(_))
61    }
62
63    /// Check if the error is task-not-found
64    #[inline]
65    #[must_use]
66    pub const fn is_task_not_found(&self) -> bool {
67        matches!(self, CelersError::TaskNotFound(_))
68    }
69
70    /// Check if the error is task-execution-related
71    #[inline]
72    #[must_use]
73    pub const fn is_task_execution(&self) -> bool {
74        matches!(self, CelersError::TaskExecution(_))
75    }
76
77    /// Check if the error is task-revoked
78    #[inline]
79    #[must_use]
80    pub const fn is_task_revoked(&self) -> bool {
81        matches!(self, CelersError::TaskRevoked(_))
82    }
83
84    /// Check if the error is timeout-related
85    #[inline]
86    #[must_use]
87    pub const fn is_timeout(&self) -> bool {
88        matches!(self, CelersError::Timeout(_))
89    }
90
91    /// Check if the error is configuration-related
92    #[inline]
93    #[must_use]
94    pub const fn is_configuration(&self) -> bool {
95        matches!(self, CelersError::Configuration(_))
96    }
97
98    /// Check if the error is IO-related
99    #[inline]
100    #[must_use]
101    pub const fn is_io(&self) -> bool {
102        matches!(self, CelersError::Io(_))
103    }
104
105    /// Check if the error is an invalid state transition
106    #[inline]
107    #[must_use]
108    pub const fn is_invalid_state_transition(&self) -> bool {
109        matches!(self, CelersError::InvalidStateTransition { .. })
110    }
111
112    /// Check if this is a retryable error
113    ///
114    /// Returns true for broker errors and IO errors, which are typically transient.
115    /// Returns false for serialization, configuration, and state transition errors.
116    #[inline]
117    #[must_use]
118    pub const fn is_retryable(&self) -> bool {
119        matches!(
120            self,
121            CelersError::Broker(_) | CelersError::Io(_) | CelersError::TaskExecution(_)
122        )
123    }
124
125    /// Get the error category as a string
126    #[inline]
127    #[must_use]
128    pub const fn category(&self) -> &'static str {
129        match self {
130            CelersError::Serialization(_) => "serialization",
131            CelersError::Deserialization(_) => "deserialization",
132            CelersError::Broker(_) => "broker",
133            CelersError::TaskNotFound(_) => "task_not_found",
134            CelersError::TaskExecution(_) => "task_execution",
135            CelersError::TaskRevoked(_) => "task_revoked",
136            CelersError::Timeout(_) => "timeout",
137            CelersError::InvalidStateTransition { .. } => "invalid_state_transition",
138            CelersError::Configuration(_) => "configuration",
139            CelersError::Io(_) => "io",
140            CelersError::Other(_) => "other",
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_serialization_error() {
151        let err = CelersError::Serialization("test".to_string());
152        assert!(err.is_serialization());
153        assert!(!err.is_deserialization());
154        assert!(!err.is_broker());
155        assert!(!err.is_retryable());
156        assert_eq!(err.category(), "serialization");
157        assert_eq!(err.to_string(), "Serialization error: test");
158    }
159
160    #[test]
161    fn test_deserialization_error() {
162        let err = CelersError::Deserialization("invalid json".to_string());
163        assert!(err.is_deserialization());
164        assert!(!err.is_serialization());
165        assert!(!err.is_retryable());
166        assert_eq!(err.category(), "deserialization");
167    }
168
169    #[test]
170    fn test_broker_error() {
171        let err = CelersError::Broker("connection lost".to_string());
172        assert!(err.is_broker());
173        assert!(!err.is_serialization());
174        assert!(err.is_retryable()); // Broker errors are retryable
175        assert_eq!(err.category(), "broker");
176    }
177
178    #[test]
179    fn test_task_not_found_error() {
180        let err = CelersError::TaskNotFound("task123".to_string());
181        assert!(err.is_task_not_found());
182        assert!(!err.is_task_execution());
183        assert!(!err.is_retryable());
184        assert_eq!(err.category(), "task_not_found");
185    }
186
187    #[test]
188    fn test_task_execution_error() {
189        let err = CelersError::TaskExecution("panic occurred".to_string());
190        assert!(err.is_task_execution());
191        assert!(!err.is_task_not_found());
192        assert!(err.is_retryable()); // Task execution errors are retryable
193        assert_eq!(err.category(), "task_execution");
194    }
195
196    #[test]
197    fn test_invalid_state_transition_error() {
198        let err = CelersError::InvalidStateTransition {
199            from: "pending".to_string(),
200            to: "success".to_string(),
201        };
202        assert!(err.is_invalid_state_transition());
203        assert!(!err.is_retryable());
204        assert_eq!(err.category(), "invalid_state_transition");
205        assert!(err
206            .to_string()
207            .contains("Invalid task state transition from"));
208    }
209
210    #[test]
211    fn test_configuration_error() {
212        let err = CelersError::Configuration("missing redis url".to_string());
213        assert!(err.is_configuration());
214        assert!(!err.is_retryable());
215        assert_eq!(err.category(), "configuration");
216    }
217
218    #[test]
219    fn test_io_error() {
220        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
221        let err = CelersError::from(io_err);
222        assert!(err.is_io());
223        assert!(err.is_retryable()); // IO errors are retryable
224        assert_eq!(err.category(), "io");
225    }
226
227    #[test]
228    fn test_other_error() {
229        let err = CelersError::Other("unknown error".to_string());
230        assert!(!err.is_serialization());
231        assert!(!err.is_broker());
232        assert!(!err.is_retryable());
233        assert_eq!(err.category(), "other");
234    }
235
236    #[test]
237    fn test_is_retryable_logic() {
238        // Retryable errors
239        assert!(CelersError::Broker("timeout".to_string()).is_retryable());
240        assert!(CelersError::TaskExecution("temporary failure".to_string()).is_retryable());
241        assert!(CelersError::from(std::io::Error::new(
242            std::io::ErrorKind::ConnectionAborted,
243            "connection aborted"
244        ))
245        .is_retryable());
246
247        // Non-retryable errors
248        assert!(!CelersError::Serialization("bad format".to_string()).is_retryable());
249        assert!(!CelersError::Configuration("invalid config".to_string()).is_retryable());
250        assert!(!CelersError::InvalidStateTransition {
251            from: "a".to_string(),
252            to: "b".to_string()
253        }
254        .is_retryable());
255    }
256}