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