flag_rs/
error.rs

1//! Error types for the flag framework
2//!
3//! This module defines the error types that can occur when parsing commands,
4//! flags, and arguments, or when executing command handlers.
5
6use std::fmt;
7
8/// The main error type for the flag framework
9///
10/// This enum represents all possible errors that can occur during command
11/// parsing, validation, and execution.
12#[derive(Debug)]
13pub enum Error {
14    /// The specified command was not found
15    ///
16    /// This error occurs when a user tries to run a subcommand that doesn't exist.
17    CommandNotFound {
18        /// The name of the unknown command
19        command: String,
20        /// Suggested similar commands
21        suggestions: Vec<String>,
22    },
23
24    /// A command requires a subcommand but none was provided
25    ///
26    /// This error occurs when a parent command has no run function and the user
27    /// doesn't specify which subcommand to run. Contains the parent command name.
28    SubcommandRequired(String),
29
30    /// A command has no run function defined
31    ///
32    /// This error occurs when trying to execute a command that has no run handler.
33    /// Contains the command name.
34    NoRunFunction(String),
35
36    /// An error occurred while parsing flag values
37    ///
38    /// This error occurs when a flag value cannot be parsed as the expected type
39    /// (e.g., "abc" for an integer flag).
40    FlagParsing {
41        /// Description of the error
42        message: String,
43        /// The flag that caused the error
44        flag: Option<String>,
45        /// Suggested valid values or format
46        suggestions: Vec<String>,
47    },
48
49    /// An error occurred while parsing command arguments
50    ///
51    /// This error occurs when command arguments don't meet requirements.
52    /// Contains a description of the error.
53    ArgumentParsing(String),
54
55    /// Argument validation failed
56    ///
57    /// This error occurs when arguments don't meet validation constraints.
58    ArgumentValidation {
59        /// Description of the validation failure
60        message: String,
61        /// Expected argument pattern
62        expected: String,
63        /// Number of arguments received
64        received: usize,
65    },
66
67    /// A validation error occurred
68    ///
69    /// This error occurs when custom validation logic fails.
70    /// Contains a description of the validation failure.
71    Validation(String),
72
73    /// An error occurred during shell completion
74    ///
75    /// This error occurs when completion functions fail.
76    /// Contains a description of the error.
77    Completion(String),
78
79    /// An I/O error occurred
80    ///
81    /// Wraps standard I/O errors that may occur during command execution.
82    Io(std::io::Error),
83
84    /// A custom error from user code
85    ///
86    /// Allows command handlers to return their own error types.
87    Custom(Box<dyn std::error::Error + Send + Sync>),
88}
89
90impl fmt::Display for Error {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        use crate::color;
93
94        match self {
95            Self::CommandNotFound {
96                command,
97                suggestions,
98            } => {
99                write!(f, "{}: unknown command ", color::red("Error"))?;
100                write!(f, "{}", color::bold(command))?;
101
102                if !suggestions.is_empty() {
103                    write!(f, "\n\n")?;
104                    if suggestions.len() == 1 {
105                        writeln!(f, "{}?", color::yellow("Did you mean this"))?;
106                        write!(f, "    {}", color::green(&suggestions[0]))?;
107                    } else {
108                        writeln!(f, "{}?", color::yellow("Did you mean one of these"))?;
109                        for suggestion in suggestions {
110                            writeln!(f, "    {}", color::green(suggestion))?;
111                        }
112                        // Remove trailing newline
113                        if f.width().is_none() {
114                            // This is a hack to remove the last newline
115                            // In real usage, the error display adds its own newline
116                        }
117                    }
118                }
119                Ok(())
120            }
121            Self::SubcommandRequired(cmd) => {
122                write!(f, "{}: ", color::red("Error"))?;
123                write!(f, "'{}' requires a subcommand", color::bold(cmd))?;
124                write!(
125                    f,
126                    "\n\n{}: use '{} --help' for available subcommands",
127                    color::yellow("Hint"),
128                    cmd
129                )
130            }
131            Self::NoRunFunction(cmd) => {
132                write!(
133                    f,
134                    "{}: command '{}' is not runnable",
135                    color::red("Error"),
136                    color::bold(cmd)
137                )
138            }
139            Self::FlagParsing {
140                message,
141                flag,
142                suggestions,
143            } => {
144                write!(f, "{}: {}", color::red("Error"), message)?;
145                if let Some(flag_name) = flag {
146                    write!(f, " for flag '{}'", color::bold(flag_name))?;
147                }
148
149                if !suggestions.is_empty() {
150                    write!(f, "\n\n")?;
151                    if suggestions.len() == 1 {
152                        write!(f, "{}: {}", color::yellow("Expected"), suggestions[0])?;
153                    } else {
154                        writeln!(f, "{} one of:", color::yellow("Expected"))?;
155                        for suggestion in suggestions {
156                            writeln!(f, "    {}", color::green(suggestion))?;
157                        }
158                        // Remove trailing newline
159                        if f.width().is_none() {
160                            // Handled by caller
161                        }
162                    }
163                }
164                Ok(())
165            }
166            Self::ArgumentParsing(msg) => {
167                write!(f, "{}: invalid argument - {}", color::red("Error"), msg)
168            }
169            Self::ArgumentValidation {
170                message,
171                expected,
172                received,
173            } => {
174                write!(f, "{}: {}", color::red("Error"), message)?;
175                write!(f, "\n\n{}: {}", color::yellow("Expected"), expected)?;
176                write!(
177                    f,
178                    "\n{}: {} argument{}",
179                    color::yellow("Received"),
180                    received,
181                    if *received == 1 { "" } else { "s" }
182                )
183            }
184            Self::Validation(msg) => {
185                write!(f, "{}: {}", color::red("Validation error"), msg)
186            }
187            Self::Completion(msg) => {
188                write!(f, "{}: {}", color::red("Completion error"), msg)
189            }
190            Self::Io(err) => {
191                write!(f, "{}: {}", color::red("IO error"), err)
192            }
193            Self::Custom(err) => {
194                write!(f, "{}: {}", color::red("Error"), err)
195            }
196        }
197    }
198}
199
200impl std::error::Error for Error {
201    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
202        match self {
203            Self::Io(err) => Some(err),
204            Self::Custom(err) => Some(err.as_ref()),
205            _ => None,
206        }
207    }
208}
209
210impl From<std::io::Error> for Error {
211    fn from(err: std::io::Error) -> Self {
212        Self::Io(err)
213    }
214}
215
216/// Type alias for Results with the flag Error type
217///
218/// This is a convenience type alias for `std::result::Result<T, flag::Error>`.
219///
220/// # Examples
221///
222/// ```
223/// use flag_rs::error::{Error, Result};
224///
225/// fn parse_count(s: &str) -> Result<u32> {
226///     s.parse()
227///         .map_err(|_| Error::ArgumentParsing(format!("invalid count: {}", s)))
228/// }
229/// ```
230pub type Result<T> = std::result::Result<T, Error>;
231
232impl Error {
233    /// Create a simple flag parsing error
234    pub fn flag_parsing(message: impl Into<String>) -> Self {
235        Self::FlagParsing {
236            message: message.into(),
237            flag: None,
238            suggestions: vec![],
239        }
240    }
241
242    /// Create a flag parsing error with suggestions
243    pub fn flag_parsing_with_suggestions(
244        message: impl Into<String>,
245        flag: impl Into<String>,
246        suggestions: Vec<String>,
247    ) -> Self {
248        Self::FlagParsing {
249            message: message.into(),
250            flag: Some(flag.into()),
251            suggestions,
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use std::error::Error as StdError;
260
261    #[test]
262    fn test_error_display() {
263        // Test without color for predictable test output
264        unsafe { std::env::set_var("NO_COLOR", "1") };
265
266        assert_eq!(
267            Error::CommandNotFound {
268                command: "test".to_string(),
269                suggestions: vec![],
270            }
271            .to_string(),
272            "Error: unknown command test"
273        );
274        assert_eq!(
275            Error::SubcommandRequired("kubectl".to_string()).to_string(),
276            "Error: 'kubectl' requires a subcommand\n\nHint: use 'kubectl --help' for available subcommands"
277        );
278        assert_eq!(
279            Error::FlagParsing {
280                message: "invalid flag".to_string(),
281                flag: Some("invalid".to_string()),
282                suggestions: vec![],
283            }
284            .to_string(),
285            "Error: invalid flag for flag 'invalid'"
286        );
287
288        unsafe { std::env::remove_var("NO_COLOR") };
289    }
290
291    #[test]
292    fn test_error_source() {
293        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
294        let error = Error::Io(io_error);
295        assert!(error.source().is_some());
296
297        let error = Error::CommandNotFound {
298            command: "test".to_string(),
299            suggestions: vec![],
300        };
301        assert!(error.source().is_none());
302    }
303
304    #[test]
305    fn test_error_with_suggestions() {
306        // Test without color for predictable test output
307        unsafe { std::env::set_var("NO_COLOR", "1") };
308
309        // Single suggestion
310        let error = Error::CommandNotFound {
311            command: "strt".to_string(),
312            suggestions: vec!["start".to_string()],
313        };
314        assert_eq!(
315            error.to_string(),
316            "Error: unknown command strt\n\nDid you mean this?\n    start"
317        );
318
319        // Multiple suggestions
320        let error = Error::CommandNotFound {
321            command: "lst".to_string(),
322            suggestions: vec!["list".to_string(), "last".to_string()],
323        };
324        assert_eq!(
325            error.to_string(),
326            "Error: unknown command lst\n\nDid you mean one of these?\n    list\n    last\n"
327        );
328
329        unsafe { std::env::remove_var("NO_COLOR") };
330    }
331}