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