use std::collections::BTreeSet;
use clap::CommandFactory;
fn collect_leaf_commands(cmd: &clap::Command, prefix: Vec<String>) -> Vec<Vec<String>> {
let mut leaves = Vec::new();
for sub in cmd.get_subcommands() {
if sub.get_name() == "help" {
continue;
}
let mut child_path = prefix.clone();
child_path.push(sub.get_name().to_owned());
let real_children: Vec<_> = sub
.get_subcommands()
.filter(|c| c.get_name() != "help")
.collect();
if real_children.is_empty() {
leaves.push(child_path);
} else {
leaves.extend(collect_leaf_commands(sub, child_path));
}
}
leaves
}
fn path_str(path: &[String]) -> String {
path.join(" ")
}
const EXPECTED_LEAVES: &[&[&str]] = &[
&["quote"],
&["historical"],
&["profile"],
&["earnings"],
&["search"],
&["schema"],
&["commands"],
&["completions"],
&["company", "profile"],
&["company", "executives"],
&["company", "peers"],
&["company", "scores"],
&["company", "float"],
&["company", "rating"],
&["company", "historical-rating"],
&["market", "quote"],
&["market", "historical"],
&["market", "dividends"],
&["market", "splits"],
&["market", "price-change"],
&["market", "stock-list"],
&["fundamentals", "income-statement"],
&["fundamentals", "income-statement-as-reported"],
&["fundamentals", "balance-sheet"],
&["fundamentals", "cash-flow"],
&["fundamentals", "ratios"],
&["fundamentals", "metrics"],
&["fundamentals", "income-statement-growth"],
&["fundamentals", "balance-sheet-growth"],
&["fundamentals", "cash-flow-growth"],
&["fundamentals", "enterprise-values"],
&["fundamentals", "analyst-estimates"],
&["fundamentals", "report-dates"],
&["fundamentals", "annual-report"],
&["analyst", "price-target-consensus"],
&["analyst", "price-target-summary"],
&["analyst", "grades"],
&["insider", "latest"],
&["calendar", "earnings"],
&["macro", "treasury-rates"],
&["macro", "economic-indicators"],
&["technical", "sma"],
&["sec", "filings"],
&["etf", "holdings"],
&["crypto", "list"],
&["crypto", "quote"],
&["crypto", "historical"],
&["forex", "quote"],
&["forex", "historical"],
&["news", "stock"],
&["news", "general"],
&["news", "articles"],
&["news", "forex"],
&["news", "crypto"],
];
#[test]
fn leaf_commands_match_expected() {
let cmd = <rusty_fmp::Cli as CommandFactory>::command();
let actual: BTreeSet<String> = collect_leaf_commands(&cmd, Vec::new())
.iter()
.map(|p| path_str(p))
.collect();
let expected: BTreeSet<String> = EXPECTED_LEAVES
.iter()
.map(|parts| parts.join(" "))
.collect();
let missing: BTreeSet<_> = expected.difference(&actual).collect();
let unexpected: BTreeSet<_> = actual.difference(&expected).collect();
assert!(
missing.is_empty() && unexpected.is_empty(),
"leaf command mismatch\n missing: {missing:?}\n unexpected: {unexpected:?}"
);
assert_eq!(
actual.len(),
EXPECTED_LEAVES.len(),
"expected {} leaves, found {}",
EXPECTED_LEAVES.len(),
actual.len()
);
}
#[test]
fn every_leaf_has_about() {
let cmd = <rusty_fmp::Cli as CommandFactory>::command();
check_leaf_help(&cmd, Vec::new(), &HelpField::About);
}
#[test]
fn every_leaf_has_long_about() {
let cmd = <rusty_fmp::Cli as CommandFactory>::command();
check_leaf_help(&cmd, Vec::new(), &HelpField::LongAbout);
}
enum HelpField {
About,
LongAbout,
}
fn check_leaf_help(cmd: &clap::Command, prefix: Vec<String>, field: &HelpField) {
for sub in cmd.get_subcommands() {
if sub.get_name() == "help" {
continue;
}
let mut child_path = prefix.clone();
child_path.push(sub.get_name().to_owned());
let real_children: Vec<_> = sub
.get_subcommands()
.filter(|c| c.get_name() != "help")
.collect();
if real_children.is_empty() {
let display = path_str(&child_path);
match field {
HelpField::About => {
assert!(
sub.get_about().is_some(),
"leaf `{display}` is missing `about`"
);
}
HelpField::LongAbout => {
assert!(
sub.get_long_about().is_some(),
"leaf `{display}` is missing `long_about`"
);
}
}
} else {
check_leaf_help(sub, child_path, field);
}
}
}
#[test]
fn no_flat_kebab_commands_at_top_level() {
let cmd = <rusty_fmp::Cli as CommandFactory>::command();
for sub in cmd.get_subcommands() {
let name = sub.get_name();
if name == "help" {
continue;
}
assert!(
!name.contains('-'),
"top-level command `{name}` looks like a flat kebab-case name; \
commands should be grouped (e.g. `market quote`) or single-word aliases"
);
}
}