use std::collections::HashMap;
use clap_usage::usage;
use clap_usage::usage::SpecCommandEffect::{self, Destructive, Read, Write};
pub const EFFECTS: &[(&str, SpecCommandEffect)] = &[
("generate", Read),
("init", Write),
("sponsors", Read),
("usage", Read),
];
pub const FLAG_EFFECTS: &[(&str, &str, SpecCommandEffect)] = &[
("generate", "changelog", Write),
("generate", "github-release", Write),
("generate", "output", Write),
("init", "force", Destructive),
];
pub fn apply(spec: &mut usage::Spec) {
let effects: HashMap<&str, SpecCommandEffect> = EFFECTS.iter().copied().collect();
annotate(&mut spec.cmd, &mut vec![], &effects);
}
fn annotate(
cmd: &mut usage::SpecCommand,
path: &mut Vec<String>,
effects: &HashMap<&str, SpecCommandEffect>,
) {
for (name, sub) in cmd.subcommands.iter_mut() {
path.push(name.clone());
let full = path.join(" ");
if let Some(effect) = effects.get(full.as_str()) {
sub.effect = Some(*effect);
}
for (cmd_path, flag_name, effect) in FLAG_EFFECTS {
if *cmd_path != full {
continue;
}
if let Some(flag) = sub.flags.iter_mut().find(|f| f.name == *flag_name) {
flag.effect = Some(*effect);
}
}
annotate(sub, path, effects);
path.pop();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::Cli;
use clap::CommandFactory;
use std::collections::HashSet;
fn all_commands() -> Vec<String> {
let spec: usage::Spec = Cli::command().into();
let mut out = vec![];
collect(&spec.cmd, &mut vec![], &mut out);
out
}
fn collect(cmd: &usage::SpecCommand, path: &mut Vec<String>, out: &mut Vec<String>) {
for (name, sub) in &cmd.subcommands {
path.push(name.clone());
out.push(path.join(" "));
collect(sub, path, out);
path.pop();
}
}
#[test]
fn every_command_is_classified() {
let known: HashSet<&str> = EFFECTS.iter().map(|(name, _)| *name).collect();
let missing: Vec<String> = all_commands()
.into_iter()
.filter(|cmd| !known.contains(cmd.as_str()))
.collect();
let detail = missing.join("\n ");
assert!(
missing.is_empty(),
"these commands have no entry in EFFECTS (src/command_effects.rs) — \
decide whether each is read, write or destructive:\n {detail}"
);
}
#[test]
fn no_classification_refers_to_a_missing_command() {
let present: HashSet<String> = all_commands().into_iter().collect();
let stale: Vec<&str> = EFFECTS
.iter()
.map(|(name, _)| *name)
.filter(|name| !present.contains(*name))
.collect();
let detail = stale.join("\n ");
assert!(
stale.is_empty(),
"these entries no longer match a command:\n {detail}"
);
}
#[test]
fn apply_annotates_commands_and_flags() {
let mut spec: usage::Spec = Cli::command().into();
apply(&mut spec);
let generate = &spec.cmd.subcommands["generate"];
assert_eq!(generate.effect, Some(Read));
let flag = |name: &str| {
generate
.flags
.iter()
.find(|f| f.name == name)
.unwrap_or_else(|| panic!("no --{name}"))
};
assert_eq!(flag("changelog").effect, Some(Write));
assert_eq!(flag("github-release").effect, Some(Write));
assert_eq!(flag("concise").effect, None);
let init = &spec.cmd.subcommands["init"];
assert_eq!(init.effect, Some(Write));
assert_eq!(
init.flags
.iter()
.find(|f| f.name == "force")
.unwrap()
.effect,
Some(Destructive)
);
}
#[test]
fn every_flag_effect_matches_a_real_flag() {
let spec: usage::Spec = Cli::command().into();
let missing: Vec<String> = FLAG_EFFECTS
.iter()
.filter_map(|(cmd_path, flag_name, _)| {
let found = spec
.cmd
.subcommands
.get(*cmd_path)
.is_some_and(|c| c.flags.iter().any(|f| f.name == *flag_name));
(!found).then_some(format!("{cmd_path} --{flag_name}"))
})
.collect();
let detail = missing.join("\n ");
assert!(
missing.is_empty(),
"these FLAG_EFFECTS entries do not match a real flag:\n {detail}"
);
}
#[test]
fn classifications_are_not_duplicated() {
let mut seen = HashSet::new();
for (name, _) in EFFECTS {
assert!(seen.insert(name), "{name} is classified twice");
}
}
}