use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Debug, Parser)]
#[command(
name = "smackdebt",
version,
about = "Find costly code and see whether a change made it better"
)]
pub(crate) struct Cli {
#[command(subcommand)]
pub(crate) command: Option<Command>,
pub(crate) path: Option<PathBuf>,
#[command(flatten)]
pub(crate) common: Common,
}
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
Init(crate::init::InitArgs),
Diff(DiffArgs),
Gate(GateArgs),
}
#[derive(Debug, Args)]
pub(crate) struct DiffArgs {
pub(crate) reference: Option<String>,
pub(crate) path: Option<PathBuf>,
#[command(flatten)]
pub(crate) common: Common,
}
#[derive(Debug, Args)]
pub(crate) struct GateArgs {
pub(crate) path: Option<PathBuf>,
#[arg(long)]
pub(crate) baseline: Option<PathBuf>,
#[arg(long, conflicts_with = "json")]
pub(crate) update: bool,
#[arg(long)]
pub(crate) json: bool,
#[arg(long, value_parser = parse_jobs)]
pub(crate) jobs: Option<usize>,
}
#[derive(Clone, Debug, Args)]
pub(crate) struct Common {
#[arg(long)]
pub(crate) json: bool,
#[arg(long, value_parser = parse_jobs)]
pub(crate) jobs: Option<usize>,
#[arg(long, value_parser = parse_days)]
pub(crate) history: Option<u32>,
#[arg(long)]
pub(crate) all: bool,
#[arg(long, value_parser = parse_top, conflicts_with_all = ["json", "all"])]
pub(crate) top: Option<usize>,
#[arg(long, value_enum, conflicts_with = "json")]
pub(crate) color: Option<ColorChoice>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub(crate) enum ColorChoice {
Auto,
Always,
Never,
}
pub(crate) fn parse_days(value: &str) -> Result<u32, String> {
let days = value
.strip_suffix('d')
.ok_or_else(|| "use days, for example 90d".to_owned())?
.parse::<u32>()
.map_err(|_| "use a whole number of days".to_owned())?;
if days == 0 {
return Err("use at least one day".to_owned());
}
Ok(days)
}
fn parse_top(value: &str) -> Result<usize, String> {
let top = value
.parse::<usize>()
.map_err(|_| "use a whole number greater than zero".to_owned())?;
if top == 0 {
return Err("use a whole number greater than zero".to_owned());
}
Ok(top)
}
fn parse_jobs(value: &str) -> Result<usize, String> {
let jobs = value
.parse::<usize>()
.map_err(|_| "use a whole number greater than zero".to_owned())?;
if jobs == 0 {
return Err("use a whole number greater than zero".to_owned());
}
Ok(jobs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_history_days() {
assert_eq!(parse_days("180d"), Ok(180));
assert!(parse_days("0d").is_err());
assert!(parse_days("90").is_err());
}
#[test]
fn rejects_zero_jobs() {
assert!(Cli::try_parse_from(["smackdebt", "--jobs", "0"]).is_err());
}
#[test]
fn all_and_json_are_rejected_by_the_command_not_by_usage_text() {
let cli = Cli::try_parse_from(["smackdebt", "--json", "--all"]).unwrap();
assert!(cli.common.json && cli.common.all);
}
#[test]
fn explicit_color_conflicts_with_json() {
assert!(Cli::try_parse_from(["smackdebt", "--json", "--color", "always"]).is_err());
}
#[test]
fn rejects_a_zero_finding_limit() {
assert!(Cli::try_parse_from(["smackdebt", "--top", "0"]).is_err());
let cli = Cli::try_parse_from(["smackdebt", "--top", "5"]).unwrap();
assert_eq!(cli.common.top, Some(5));
}
#[test]
fn a_baseline_update_conflicts_with_gate_json() {
assert!(Cli::try_parse_from(["smackdebt", "gate", "--update", "--json"]).is_err());
let cli = Cli::try_parse_from(["smackdebt", "gate", "--update"]).unwrap();
let Some(Command::Gate(args)) = cli.command else {
panic!("expected the gate subcommand");
};
assert!(args.update && !args.json);
}
#[test]
fn a_finding_limit_conflicts_with_json_and_with_all() {
assert!(Cli::try_parse_from(["smackdebt", "--top", "5", "--json"]).is_err());
assert!(Cli::try_parse_from(["smackdebt", "--top", "5", "--all"]).is_err());
}
}