use std::fmt;
#[derive(Debug)]
pub enum Error {
CommandNotFound {
command: String,
suggestions: Vec<String>,
},
SubcommandRequired(String),
NoRunFunction(String),
FlagParsing {
message: String,
flag: Option<String>,
suggestions: Vec<String>,
},
ArgumentParsing(String),
ArgumentValidation {
message: String,
expected: String,
received: usize,
},
Validation(String),
Completion(String),
Io(std::io::Error),
Custom(Box<dyn std::error::Error + Send + Sync>),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use crate::color;
match self {
Self::CommandNotFound {
command,
suggestions,
} => {
write!(f, "{}: unknown command ", color::red("Error"))?;
write!(f, "{}", color::bold(command))?;
if !suggestions.is_empty() {
write!(f, "\n\n")?;
if suggestions.len() == 1 {
writeln!(f, "{}?", color::yellow("Did you mean this"))?;
write!(f, " {}", color::green(&suggestions[0]))?;
} else {
writeln!(f, "{}?", color::yellow("Did you mean one of these"))?;
for suggestion in suggestions {
writeln!(f, " {}", color::green(suggestion))?;
}
}
}
Ok(())
}
Self::SubcommandRequired(cmd) => {
write!(f, "{}: ", color::red("Error"))?;
write!(f, "'{}' requires a subcommand", color::bold(cmd))?;
write!(
f,
"\n\n{}: use '{} --help' for available subcommands",
color::yellow("Hint"),
cmd
)
}
Self::NoRunFunction(cmd) => {
write!(
f,
"{}: command '{}' is not runnable",
color::red("Error"),
color::bold(cmd)
)
}
Self::FlagParsing {
message,
flag,
suggestions,
} => {
write!(f, "{}: {}", color::red("Error"), message)?;
if let Some(flag_name) = flag {
write!(f, " for flag '{}'", color::bold(flag_name))?;
}
if !suggestions.is_empty() {
write!(f, "\n\n")?;
if suggestions.len() == 1 {
write!(f, "{}: {}", color::yellow("Expected"), suggestions[0])?;
} else {
writeln!(f, "{} one of:", color::yellow("Expected"))?;
for suggestion in suggestions {
writeln!(f, " {}", color::green(suggestion))?;
}
}
}
Ok(())
}
Self::ArgumentParsing(msg) => {
write!(f, "{}: invalid argument - {}", color::red("Error"), msg)
}
Self::ArgumentValidation {
message,
expected,
received,
} => {
write!(f, "{}: {}", color::red("Error"), message)?;
write!(f, "\n\n{}: {}", color::yellow("Expected"), expected)?;
write!(
f,
"\n{}: {} argument{}",
color::yellow("Received"),
received,
if *received == 1 { "" } else { "s" }
)
}
Self::Validation(msg) => {
write!(f, "{}: {}", color::red("Validation error"), msg)
}
Self::Completion(msg) => {
write!(f, "{}: {}", color::red("Completion error"), msg)
}
Self::Io(err) => {
write!(f, "{}: {}", color::red("IO error"), err)
}
Self::Custom(err) => {
write!(f, "{}: {}", color::red("Error"), err)
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(err) => Some(err),
Self::Custom(err) => Some(err.as_ref()),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
pub type Result<T> = std::result::Result<T, Error>;
impl Error {
pub fn flag_parsing(message: impl Into<String>) -> Self {
Self::FlagParsing {
message: message.into(),
flag: None,
suggestions: vec![],
}
}
pub fn flag_parsing_with_suggestions(
message: impl Into<String>,
flag: impl Into<String>,
suggestions: Vec<String>,
) -> Self {
Self::FlagParsing {
message: message.into(),
flag: Some(flag.into()),
suggestions,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as StdError;
#[test]
fn test_error_display() {
unsafe { std::env::set_var("NO_COLOR", "1") };
assert_eq!(
Error::CommandNotFound {
command: "test".to_string(),
suggestions: vec![],
}
.to_string(),
"Error: unknown command test"
);
assert_eq!(
Error::SubcommandRequired("kubectl".to_string()).to_string(),
"Error: 'kubectl' requires a subcommand\n\nHint: use 'kubectl --help' for available subcommands"
);
assert_eq!(
Error::FlagParsing {
message: "invalid flag".to_string(),
flag: Some("invalid".to_string()),
suggestions: vec![],
}
.to_string(),
"Error: invalid flag for flag 'invalid'"
);
unsafe { std::env::remove_var("NO_COLOR") };
}
#[test]
fn test_error_source() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let error = Error::Io(io_error);
assert!(error.source().is_some());
let error = Error::CommandNotFound {
command: "test".to_string(),
suggestions: vec![],
};
assert!(error.source().is_none());
}
#[test]
fn test_error_with_suggestions() {
unsafe { std::env::set_var("NO_COLOR", "1") };
let error = Error::CommandNotFound {
command: "strt".to_string(),
suggestions: vec!["start".to_string()],
};
assert_eq!(
error.to_string(),
"Error: unknown command strt\n\nDid you mean this?\n start"
);
let error = Error::CommandNotFound {
command: "lst".to_string(),
suggestions: vec!["list".to_string(), "last".to_string()],
};
assert_eq!(
error.to_string(),
"Error: unknown command lst\n\nDid you mean one of these?\n list\n last\n"
);
unsafe { std::env::remove_var("NO_COLOR") };
}
}