use clap::Parser;
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct Cli {
pub format: OutputFormat,
pub verbosity: u8,
pub color: bool,
}
impl Default for Cli {
fn default() -> Self {
Self {
format: OutputFormat::Table,
verbosity: 1,
color: true,
}
}
}
impl Cli {
pub fn new() -> Self {
Self::default()
}
pub fn with_format(mut self, format: OutputFormat) -> Self {
self.format = format;
self
}
pub fn with_verbosity(mut self, level: u8) -> Self {
self.verbosity = level;
self
}
pub fn with_color(mut self, enabled: bool) -> Self {
self.color = enabled;
self
}
pub fn is_quiet(&self) -> bool {
self.verbosity == 0
}
pub fn is_verbose(&self) -> bool {
self.verbosity >= 2
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputFormat {
#[default]
Table,
Json,
Compact,
}
impl FromStr for OutputFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"table" | "text" => Ok(Self::Table),
"json" => Ok(Self::Json),
"compact" | "line" => Ok(Self::Compact),
_ => Err(format!(
"Unknown output format '{s}'. Valid options: table, json, compact"
)),
}
}
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Table => write!(f, "table"),
Self::Json => write!(f, "json"),
Self::Compact => write!(f, "compact"),
}
}
}
#[derive(Parser, Debug, Clone)]
pub struct CommonArgs {
#[arg(short, long, default_value = "table")]
pub format: String,
#[arg(short, long)]
pub quiet: bool,
#[arg(short, long)]
pub verbose: bool,
#[arg(long)]
pub no_color: bool,
}
impl CommonArgs {
pub fn to_cli(&self) -> Cli {
let verbosity = if self.quiet {
0
} else if self.verbose {
2
} else {
1
};
Cli {
format: self.format.parse().unwrap_or_default(),
verbosity,
color: !self.no_color,
}
}
}
pub mod styles {
pub struct Colors;
impl Colors {
pub const RESET: &'static str = "\x1b[0m";
pub const BOLD: &'static str = "\x1b[1m";
pub const DIM: &'static str = "\x1b[2m";
pub const RED: &'static str = "\x1b[31m";
pub const GREEN: &'static str = "\x1b[32m";
pub const YELLOW: &'static str = "\x1b[33m";
pub const BLUE: &'static str = "\x1b[34m";
pub const CYAN: &'static str = "\x1b[36m";
}
pub fn success(msg: &str) -> String {
format!("{}✓{} {}", Colors::GREEN, Colors::RESET, msg)
}
pub fn error(msg: &str) -> String {
format!("{}✗{} {}", Colors::RED, Colors::RESET, msg)
}
pub fn warning(msg: &str) -> String {
format!("{}⚠{} {}", Colors::YELLOW, Colors::RESET, msg)
}
pub fn info(msg: &str) -> String {
format!("{}ℹ{} {}", Colors::BLUE, Colors::RESET, msg)
}
pub fn header(msg: &str) -> String {
format!("{}{}{}", Colors::BOLD, msg, Colors::RESET)
}
pub fn dim(msg: &str) -> String {
format!("{}{}{}", Colors::DIM, msg, Colors::RESET)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_output_format_roundtrip() {
for format in [
OutputFormat::Table,
OutputFormat::Json,
OutputFormat::Compact,
] {
let s = format.to_string();
let parsed: OutputFormat = s.parse().expect("parsing should succeed");
assert_eq!(format, parsed);
}
}
#[test]
fn test_output_format_case_insensitive() {
assert_eq!(
"JSON"
.parse::<OutputFormat>()
.expect("parsing should succeed"),
OutputFormat::Json
);
assert_eq!(
"Table"
.parse::<OutputFormat>()
.expect("parsing should succeed"),
OutputFormat::Table
);
assert_eq!(
"COMPACT"
.parse::<OutputFormat>()
.expect("parsing should succeed"),
OutputFormat::Compact
);
}
#[test]
fn test_cli_verbosity_levels() {
let quiet = Cli::new().with_verbosity(0);
assert!(quiet.is_quiet());
assert!(!quiet.is_verbose());
let normal = Cli::new().with_verbosity(1);
assert!(!normal.is_quiet());
assert!(!normal.is_verbose());
let verbose = Cli::new().with_verbosity(2);
assert!(!verbose.is_quiet());
assert!(verbose.is_verbose());
}
#[test]
fn test_styles_include_ansi_codes() {
let success = styles::success("done");
assert!(success.contains('\x1b'));
assert!(success.contains("done"));
assert!(success.contains('✓'));
}
#[test]
fn test_common_args_to_cli() {
let args = CommonArgs {
format: "json".to_string(),
quiet: false,
verbose: true,
no_color: true,
};
let cli = args.to_cli();
assert_eq!(cli.format, OutputFormat::Json);
assert_eq!(cli.verbosity, 2);
assert!(!cli.color);
}
#[test]
fn test_output_format_aliases() {
assert_eq!(
"text"
.parse::<OutputFormat>()
.expect("parsing should succeed"),
OutputFormat::Table
);
assert_eq!(
"line"
.parse::<OutputFormat>()
.expect("parsing should succeed"),
OutputFormat::Compact
);
}
#[test]
fn test_output_format_invalid() {
let result = "invalid_format".parse::<OutputFormat>();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("Unknown output format"));
assert!(err.contains("invalid_format"));
}
#[test]
fn test_cli_default_values() {
let cli = Cli::default();
assert_eq!(cli.format, OutputFormat::Table);
assert_eq!(cli.verbosity, 1);
assert!(cli.color);
assert!(!cli.is_quiet());
assert!(!cli.is_verbose());
}
#[test]
fn test_cli_builder_pattern() {
let cli = Cli::new()
.with_format(OutputFormat::Json)
.with_verbosity(2)
.with_color(false);
assert_eq!(cli.format, OutputFormat::Json);
assert_eq!(cli.verbosity, 2);
assert!(!cli.color);
}
#[test]
fn test_styles_all_variants() {
let success = styles::success("ok");
assert!(success.contains('✓'));
assert!(success.contains(styles::Colors::GREEN));
let error = styles::error("fail");
assert!(error.contains('✗'));
assert!(error.contains(styles::Colors::RED));
let warning = styles::warning("warn");
assert!(warning.contains('⚠'));
assert!(warning.contains(styles::Colors::YELLOW));
let info = styles::info("note");
assert!(info.contains('ℹ'));
assert!(info.contains(styles::Colors::BLUE));
let header = styles::header("title");
assert!(header.contains("title"));
assert!(header.contains(styles::Colors::BOLD));
let dim = styles::dim("secondary");
assert!(dim.contains("secondary"));
assert!(dim.contains(styles::Colors::DIM));
}
#[test]
fn test_common_args_quiet_mode() {
let args = CommonArgs {
format: "table".to_string(),
quiet: true,
verbose: false,
no_color: false,
};
let cli = args.to_cli();
assert_eq!(cli.verbosity, 0);
assert!(cli.is_quiet());
}
#[test]
fn test_common_args_default_format_fallback() {
let args = CommonArgs {
format: "invalid".to_string(),
quiet: false,
verbose: false,
no_color: false,
};
let cli = args.to_cli();
assert_eq!(cli.format, OutputFormat::Table);
}
#[test]
fn test_verbosity_level_3_is_verbose() {
let cli = Cli::new().with_verbosity(3);
assert!(cli.is_verbose());
assert!(!cli.is_quiet());
}
}