Skip to main content

things3_core/
error.rs

1//! Error types for the Things Core library
2
3use thiserror::Error;
4
5/// Result type alias for Things operations
6pub type Result<T> = std::result::Result<T, ThingsError>;
7
8/// Main error type for Things operations
9#[derive(Error, Debug)]
10pub enum ThingsError {
11    #[error("Database error: {0}")]
12    Database(String),
13
14    #[error("Serialization error: {0}")]
15    Serialization(#[from] serde_json::Error),
16
17    #[error("IO error: {0}")]
18    Io(#[from] std::io::Error),
19
20    #[error("Database not found: {path}. Ensure Things 3 is installed and has been opened at least once, or specify a custom database path.")]
21    DatabaseNotFound { path: String },
22
23    #[error("Invalid UUID: {uuid}. UUIDs must be in format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
24    InvalidUuid { uuid: String },
25
26    #[error("Invalid date: {date}")]
27    InvalidDate { date: String },
28
29    #[error("Date validation failed: {0}")]
30    DateValidation(#[from] crate::database::DateValidationError),
31
32    #[error("Date conversion failed: {0}")]
33    DateConversion(#[from] crate::database::DateConversionError),
34
35    #[error("Task not found: {uuid}. The task may have been deleted or moved. Try searching by title instead.")]
36    TaskNotFound { uuid: String },
37
38    #[error("Project not found: {uuid}. The project may have been deleted. Verify the UUID or list all projects to find the correct one.")]
39    ProjectNotFound { uuid: String },
40
41    #[error("Area not found: {uuid}. The area may have been deleted. Verify the UUID or list all areas to find the correct one.")]
42    AreaNotFound { uuid: String },
43
44    #[error("Validation error: {message}")]
45    Validation { message: String },
46
47    #[error("Invalid cursor: {0}")]
48    InvalidCursor(String),
49
50    #[error("Configuration error: {message}")]
51    Configuration { message: String },
52
53    #[error("Unknown error: {message}")]
54    Unknown { message: String },
55}
56
57impl ThingsError {
58    /// Create a validation error
59    pub fn validation(message: impl Into<String>) -> Self {
60        Self::Validation {
61            message: message.into(),
62        }
63    }
64
65    /// Create a configuration error
66    pub fn configuration(message: impl Into<String>) -> Self {
67        Self::Configuration {
68            message: message.into(),
69        }
70    }
71
72    /// Create an unknown error
73    pub fn unknown(message: impl Into<String>) -> Self {
74        Self::Unknown {
75            message: message.into(),
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use std::io;
84
85    #[test]
86    fn test_database_error_from_rusqlite() {
87        // Skip this test since rusqlite is not available in this build
88        // This test would verify rusqlite error conversion if the dependency was available
89    }
90
91    #[test]
92    fn test_serialization_error_from_serde() {
93        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
94        let things_error: ThingsError = json_error.into();
95
96        match things_error {
97            ThingsError::Serialization(_) => (),
98            _ => panic!("Expected Serialization error"),
99        }
100    }
101
102    #[test]
103    fn test_io_error_from_std() {
104        let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
105        let things_error: ThingsError = io_error.into();
106
107        match things_error {
108            ThingsError::Io(_) => (),
109            _ => panic!("Expected Io error"),
110        }
111    }
112
113    #[test]
114    fn test_database_not_found_error() {
115        let error = ThingsError::DatabaseNotFound {
116            path: "/path/to/db".to_string(),
117        };
118
119        assert!(error.to_string().contains("Database not found"));
120        assert!(error.to_string().contains("/path/to/db"));
121    }
122
123    #[test]
124    fn test_invalid_uuid_error() {
125        let error = ThingsError::InvalidUuid {
126            uuid: "invalid-uuid".to_string(),
127        };
128
129        assert!(error.to_string().contains("Invalid UUID"));
130        assert!(error.to_string().contains("invalid-uuid"));
131    }
132
133    #[test]
134    fn test_invalid_date_error() {
135        let error = ThingsError::InvalidDate {
136            date: "2023-13-45".to_string(),
137        };
138
139        assert!(error.to_string().contains("Invalid date"));
140        assert!(error.to_string().contains("2023-13-45"));
141    }
142
143    #[test]
144    fn test_task_not_found_error() {
145        let error = ThingsError::TaskNotFound {
146            uuid: "task-uuid-123".to_string(),
147        };
148
149        assert!(error.to_string().contains("Task not found"));
150        assert!(error.to_string().contains("task-uuid-123"));
151    }
152
153    #[test]
154    fn test_project_not_found_error() {
155        let error = ThingsError::ProjectNotFound {
156            uuid: "project-uuid-456".to_string(),
157        };
158
159        assert!(error.to_string().contains("Project not found"));
160        assert!(error.to_string().contains("project-uuid-456"));
161    }
162
163    #[test]
164    fn test_area_not_found_error() {
165        let error = ThingsError::AreaNotFound {
166            uuid: "area-uuid-789".to_string(),
167        };
168
169        assert!(error.to_string().contains("Area not found"));
170        assert!(error.to_string().contains("area-uuid-789"));
171    }
172
173    #[test]
174    fn test_validation_error() {
175        let error = ThingsError::Validation {
176            message: "Invalid input data".to_string(),
177        };
178
179        assert!(error.to_string().contains("Validation error"));
180        assert!(error.to_string().contains("Invalid input data"));
181    }
182
183    #[test]
184    fn test_configuration_error() {
185        let error = ThingsError::Configuration {
186            message: "Missing required config".to_string(),
187        };
188
189        assert!(error.to_string().contains("Configuration error"));
190        assert!(error.to_string().contains("Missing required config"));
191    }
192
193    #[test]
194    fn test_unknown_error() {
195        let error = ThingsError::Unknown {
196            message: "Something went wrong".to_string(),
197        };
198
199        assert!(error.to_string().contains("Unknown error"));
200        assert!(error.to_string().contains("Something went wrong"));
201    }
202
203    #[test]
204    fn test_validation_helper() {
205        let error = ThingsError::validation("Test validation message");
206
207        match error {
208            ThingsError::Validation { message } => {
209                assert_eq!(message, "Test validation message");
210            }
211            _ => panic!("Expected Validation error"),
212        }
213    }
214
215    #[test]
216    fn test_validation_helper_with_string() {
217        let message = "Test validation message".to_string();
218        let error = ThingsError::validation(message);
219
220        match error {
221            ThingsError::Validation { message } => {
222                assert_eq!(message, "Test validation message");
223            }
224            _ => panic!("Expected Validation error"),
225        }
226    }
227
228    #[test]
229    fn test_configuration_helper() {
230        let error = ThingsError::configuration("Test config message");
231
232        match error {
233            ThingsError::Configuration { message } => {
234                assert_eq!(message, "Test config message");
235            }
236            _ => panic!("Expected Configuration error"),
237        }
238    }
239
240    #[test]
241    fn test_configuration_helper_with_string() {
242        let message = "Test config message".to_string();
243        let error = ThingsError::configuration(message);
244
245        match error {
246            ThingsError::Configuration { message } => {
247                assert_eq!(message, "Test config message");
248            }
249            _ => panic!("Expected Configuration error"),
250        }
251    }
252
253    #[test]
254    fn test_unknown_helper() {
255        let error = ThingsError::unknown("Test unknown message");
256
257        match error {
258            ThingsError::Unknown { message } => {
259                assert_eq!(message, "Test unknown message");
260            }
261            _ => panic!("Expected Unknown error"),
262        }
263    }
264
265    #[test]
266    fn test_unknown_helper_with_string() {
267        let message = "Test unknown message".to_string();
268        let error = ThingsError::unknown(message);
269
270        match error {
271            ThingsError::Unknown { message } => {
272                assert_eq!(message, "Test unknown message");
273            }
274            _ => panic!("Expected Unknown error"),
275        }
276    }
277
278    #[test]
279    fn test_error_display_formatting() {
280        let errors = vec![
281            ThingsError::DatabaseNotFound {
282                path: "test.db".to_string(),
283            },
284            ThingsError::InvalidUuid {
285                uuid: "bad-uuid".to_string(),
286            },
287            ThingsError::InvalidDate {
288                date: "bad-date".to_string(),
289            },
290            ThingsError::TaskNotFound {
291                uuid: "task-123".to_string(),
292            },
293            ThingsError::ProjectNotFound {
294                uuid: "project-456".to_string(),
295            },
296            ThingsError::AreaNotFound {
297                uuid: "area-789".to_string(),
298            },
299            ThingsError::Validation {
300                message: "validation failed".to_string(),
301            },
302            ThingsError::Configuration {
303                message: "config error".to_string(),
304            },
305            ThingsError::Unknown {
306                message: "unknown error".to_string(),
307            },
308        ];
309
310        for error in errors {
311            let error_string = error.to_string();
312            assert!(!error_string.is_empty());
313            assert!(error_string.len() > 10); // Should have meaningful content
314        }
315    }
316
317    #[test]
318    fn test_error_debug_formatting() {
319        let error = ThingsError::Validation {
320            message: "test message".to_string(),
321        };
322
323        let debug_string = format!("{error:?}");
324        assert!(debug_string.contains("Validation"));
325        assert!(debug_string.contains("test message"));
326    }
327
328    #[test]
329    fn test_result_type_alias() {
330        // Test that the Result type alias works correctly
331        fn returns_result() -> String {
332            "success".to_string()
333        }
334
335        fn returns_error() -> Result<String> {
336            Err(ThingsError::validation("test error"))
337        }
338
339        assert_eq!(returns_result(), "success");
340        assert!(returns_error().is_err());
341
342        match returns_error() {
343            Err(ThingsError::Validation { message }) => {
344                assert_eq!(message, "test error");
345            }
346            _ => panic!("Expected Validation error"),
347        }
348    }
349}