use clap::Parser;
use face_core::OutputFormat;
#[derive(Debug, Parser)]
#[command(
name = "face",
version,
about = "Fold And Cluster Entries — group, page, and summarize structured command output."
)]
#[non_exhaustive]
pub struct Cli {
#[arg(long = "format-in", help_heading = "INPUT")]
pub format_in: Option<String>,
#[arg(long, help_heading = "INPUT")]
pub items: Option<String>,
#[arg(long, value_delimiter = ',', help_heading = "INPUT")]
pub columns: Option<Vec<String>>,
#[arg(long, help_heading = "SCORE")]
pub score: Option<String>,
#[arg(long, help_heading = "SCORE")]
pub scale: Option<f64>,
#[arg(long, help_heading = "SCORE")]
pub invert: bool,
#[arg(long, help_heading = "SCORE")]
pub preset: Option<String>,
#[arg(long, value_delimiter = ',', help_heading = "STRATEGY")]
pub by: Option<Vec<String>>,
#[arg(long, help_heading = "STRATEGY")]
pub strategy: Option<String>,
#[arg(long, help_heading = "STRATEGY")]
pub bands: Option<u8>,
#[arg(long, help_heading = "STRATEGY")]
pub top: Option<u32>,
#[arg(long, help_heading = "STRATEGY")]
pub within: Vec<String>,
#[arg(long, help_heading = "PAGE")]
pub cluster: Vec<String>,
#[arg(long, help_heading = "PAGE")]
pub page: Option<u32>,
#[arg(long, help_heading = "PAGE")]
pub per_page: Option<u32>,
#[arg(
long,
value_parser = parse_format,
help_heading = "OUTPUT",
)]
pub format: Option<OutputFormat>,
#[arg(long = "json", short = 'j', help_heading = "OUTPUT")]
pub json: bool,
#[arg(
long,
value_parser = clap::builder::PossibleValuesParser::new(["auto", "always", "never"]),
help_heading = "OUTPUT",
)]
pub color: Option<String>,
#[arg(long, help_heading = "MODES")]
pub explain: bool,
#[arg(long, help_heading = "MODES")]
pub schema: bool,
#[arg(long, help_heading = "MODES")]
pub interactive: bool,
#[arg(long, help_heading = "MODES")]
pub verbose: bool,
}
fn parse_format(s: &str) -> Result<OutputFormat, String> {
serde_json::from_value(serde_json::Value::String(s.to_string()))
.map_err(|e| format!("invalid format `{s}`: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn parses_minimal_invocation() {
let cli = Cli::try_parse_from(["face"]).unwrap();
assert_eq!(cli.format, None);
assert_eq!(cli.color, None);
assert!(!cli.verbose);
assert!(!cli.interactive);
assert!(!cli.explain);
assert!(!cli.json);
assert_eq!(cli.page, None);
assert_eq!(cli.per_page, None);
}
#[test]
fn parses_format_kebab_case() {
let cli = Cli::try_parse_from(["face", "--format=json-flat"]).unwrap();
assert_eq!(cli.format, Some(OutputFormat::JsonFlat));
}
#[test]
fn json_short_flag_sets_alias() {
let cli = Cli::try_parse_from(["face", "-j"]).unwrap();
assert!(cli.json);
assert_eq!(cli.format, None);
}
#[test]
fn json_long_flag_sets_alias() {
let cli = Cli::try_parse_from(["face", "--json"]).unwrap();
assert!(cli.json);
assert_eq!(cli.format, None);
}
#[test]
fn by_splits_on_comma() {
let cli = Cli::try_parse_from(["face", "--by", "file,module"]).unwrap();
assert_eq!(cli.by, Some(vec!["file".to_string(), "module".to_string()]));
}
#[test]
fn within_is_repeatable() {
let cli = Cli::try_parse_from(["face", "--within", "score", "--within", "tag"]).unwrap();
assert_eq!(cli.within, vec!["score".to_string(), "tag".to_string()]);
}
#[test]
fn cluster_is_repeatable() {
let cli = Cli::try_parse_from([
"face",
"--cluster",
"file=src/cli.rs",
"--cluster",
"score=excellent",
])
.unwrap();
assert_eq!(cli.cluster.len(), 2);
}
#[test]
fn interactive_flag_parses() {
let cli = Cli::try_parse_from(["face", "--interactive"]).unwrap();
assert!(cli.interactive);
}
#[test]
fn verbose_flag_parses() {
let cli = Cli::try_parse_from(["face", "--verbose"]).unwrap();
assert!(cli.verbose);
}
#[test]
fn quiet_flag_is_not_supported() {
let err = Cli::try_parse_from(["face", "--quiet"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
}
}