1use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, ThingsError>;
7
8#[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 pub fn validation(message: impl Into<String>) -> Self {
60 Self::Validation {
61 message: message.into(),
62 }
63 }
64
65 pub fn configuration(message: impl Into<String>) -> Self {
67 Self::Configuration {
68 message: message.into(),
69 }
70 }
71
72 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 }
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); }
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 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}