Skip to main content

yesser_todo_errors/
command_error.rs

1use std::fmt::{self, Display, Formatter};
2
3use thiserror::Error;
4
5use crate::db_error::DatabaseError;
6
7#[derive(Debug, Error)]
8pub enum CommandError {
9    NoTasksSpecified,
10    TaskExists { name: String },
11    TaskNotFound { name: String },
12    DuplicateInput { name: String },
13    DataError { what: String, err: DatabaseError },
14    HTTPError { name: String, status_code: u16 },
15    ConnectionError { name: String },
16    InvalidUrlError { why: String },
17    UnlinkedError,
18}
19
20impl CommandError {
21    /// Prints the formatted error message to standard error.
22    ///
23    /// This uses the type's `Display` implementation to produce a user-facing message.
24    ///
25    /// # Examples
26    ///
27    /// ```
28    /// use yesser_todo_errors::command_error::CommandError;
29    ///
30    /// let err = CommandError::NoTasksSpecified;
31    /// err.handle(); // prints "No tasks specified!" to stderr
32    /// ```
33    pub fn handle(&self) {
34        eprintln!("{self}")
35    }
36}
37
38impl Display for CommandError {
39    /// Format the error as a concise, user-facing message.
40    ///
41    /// # Examples
42    ///
43    /// ```
44    /// use yesser_todo_errors::command_error::CommandError;
45    ///
46    /// assert_eq!(
47    ///     format!("{}", CommandError::TaskNotFound { name: "foo".into() }),
48    ///     "Task foo not found!"
49    /// );
50    ///
51    /// assert_eq!(
52    ///     format!("{}", CommandError::NoTasksSpecified),
53    ///     "No tasks specified!"
54    /// );
55    /// ```
56    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
57        match self {
58            CommandError::NoTasksSpecified => write!(f, "No tasks specified!"),
59            CommandError::TaskExists { name } => write!(f, "Task {name} already exists!"),
60            CommandError::TaskNotFound { name } => write!(f, "Task {name} not found!"),
61            CommandError::DuplicateInput { name } => write!(f, "Task {name} was specified multiple times!"),
62            CommandError::DataError { what, err } => write!(f, "Unable to save {what}: {err}!"),
63            CommandError::HTTPError { name, status_code } => {
64                if name.is_empty() {
65                    write!(f, "HTTP error code: {status_code}!")
66                } else {
67                    write!(f, "HTTP error code: {status_code} for task {name}")
68                }
69            }
70            CommandError::ConnectionError { name } => {
71                if name.is_empty() {
72                    write!(f, "Failed to connect to the server!")
73                } else {
74                    write!(f, "Failed to connect to the server for task {name}")
75                }
76            }
77            Self::InvalidUrlError { why } => {
78                write!(f, "Invalid URL: {why}")
79            }
80            CommandError::UnlinkedError => write!(f, "You're already unlinked!"),
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_no_tasks_specified_display() {
91        let err = CommandError::NoTasksSpecified;
92        assert_eq!(format!("{}", err), "No tasks specified!");
93    }
94
95    #[test]
96    fn test_task_exists_display() {
97        let err = CommandError::TaskExists { name: "test task".to_string() };
98        assert_eq!(format!("{}", err), "Task test task already exists!");
99    }
100
101    #[test]
102    fn test_task_not_found_display() {
103        let err = CommandError::TaskNotFound { name: "missing".to_string() };
104        assert_eq!(format!("{}", err), "Task missing not found!");
105    }
106
107    #[test]
108    fn test_duplicate_input_display() {
109        let err = CommandError::DuplicateInput { name: "duplicate".to_string() };
110        assert_eq!(format!("{}", err), "Task duplicate was specified multiple times!");
111    }
112
113    #[test]
114    fn test_data_error_display() {
115        let db_err = DatabaseError::UserDirsError;
116        let err = CommandError::DataError {
117            what: "tasks".to_string(),
118            err: db_err,
119        };
120        let display = format!("{}", err);
121        assert!(display.contains("Unable to save tasks"));
122        assert!(display.contains("Could not get user config directory location"));
123    }
124
125    #[test]
126    fn test_http_error_with_name() {
127        let err = CommandError::HTTPError {
128            name: "my-task".to_string(),
129            status_code: 404,
130        };
131        assert_eq!(format!("{}", err), "HTTP error code: 404 for task my-task");
132    }
133
134    #[test]
135    fn test_http_error_without_name() {
136        let err = CommandError::HTTPError {
137            name: String::new(),
138            status_code: 500,
139        };
140        assert_eq!(format!("{}", err), "HTTP error code: 500!");
141    }
142
143    #[test]
144    fn test_connection_error_with_name() {
145        let err = CommandError::ConnectionError { name: "test-task".to_string() };
146        assert_eq!(format!("{}", err), "Failed to connect to the server for task test-task");
147    }
148
149    #[test]
150    fn test_connection_error_without_name() {
151        let err = CommandError::ConnectionError { name: String::new() };
152        assert_eq!(format!("{}", err), "Failed to connect to the server!");
153    }
154
155    #[test]
156    fn test_unlinked_error_display() {
157        let err = CommandError::UnlinkedError;
158        assert_eq!(format!("{}", err), "You're already unlinked!");
159    }
160
161    #[test]
162    fn test_command_error_is_error_trait() {
163        fn assert_error<T: std::error::Error>() {}
164        assert_error::<CommandError>();
165    }
166
167    #[test]
168    fn test_command_error_debug() {
169        let err = CommandError::NoTasksSpecified;
170        let debug_str = format!("{:?}", err);
171        assert!(debug_str.contains("NoTasksSpecified"));
172    }
173
174    #[test]
175    fn test_various_http_status_codes() {
176        let test_cases = vec![
177            (200, "HTTP error code: 200!"),
178            (400, "HTTP error code: 400!"),
179            (404, "HTTP error code: 404!"),
180            (500, "HTTP error code: 500!"),
181        ];
182
183        for (code, expected) in test_cases {
184            let err = CommandError::HTTPError {
185                name: String::new(),
186                status_code: code,
187            };
188            assert_eq!(format!("{}", err), expected);
189        }
190    }
191
192    #[test]
193    fn test_task_names_with_special_characters() {
194        let err = CommandError::TaskExists {
195            name: "task with spaces & symbols!".to_string(),
196        };
197        assert_eq!(format!("{}", err), "Task task with spaces & symbols! already exists!");
198    }
199
200    #[test]
201    fn test_data_error_with_io_error() {
202        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
203        let db_err = DatabaseError::IOError(io_err);
204        let err = CommandError::DataError {
205            what: "config".to_string(),
206            err: db_err,
207        };
208        let display = format!("{}", err);
209        assert!(display.contains("Unable to save config"));
210        assert!(display.contains("access denied"));
211    }
212
213    #[test]
214    fn test_invalid_url_error_display() {
215        let err = CommandError::InvalidUrlError {
216            why: "scheme is not http or https".to_string(),
217        };
218        assert_eq!(format!("{}", err), "Invalid URL: scheme is not http or https");
219    }
220
221    #[test]
222    fn test_invalid_url_error_with_empty_reason() {
223        let err = CommandError::InvalidUrlError { why: String::new() };
224        assert_eq!(format!("{}", err), "Invalid URL: ");
225    }
226
227    #[test]
228    fn test_invalid_url_error_with_multiline_reason() {
229        let err = CommandError::InvalidUrlError {
230            why: "invalid scheme\nHelp: use http or https".to_string(),
231        };
232        let display = format!("{}", err);
233        assert!(display.contains("Invalid URL: invalid scheme"));
234        assert!(display.contains("Help: use http or https"));
235    }
236
237    #[test]
238    fn test_invalid_url_error_debug() {
239        let err = CommandError::InvalidUrlError { why: "test".to_string() };
240        let debug_str = format!("{:?}", err);
241        assert!(debug_str.contains("InvalidUrlError"));
242        assert!(debug_str.contains("test"));
243    }
244}