Lines
100 %
Functions
81.25 %
Branches
use crate::commands::{
CliAccountBalance, CliAccountCreate, CliAccountList, CliCommand, CliCommodityCreate,
CliCommodityList, CliGetConfig, CliReportsActivity, CliReportsBalance, CliReportsBreakdown,
CliSelectColumn, CliSetConfig, CliSshKeyAdd, CliSshKeyList, CliSshKeyRemove,
CliTransactionCreate, CliTransactionList, CliVersion, CommandNode,
};
/// Canonical command tree shared between the automation CLI and the TUI
/// command palette. Keeping this in one place guarantees the same grammar
/// is exposed in both surfaces.
#[must_use]
pub fn command_tree() -> Vec<CommandNode> {
vec![
CliVersion::node(),
CommandNode {
name: "transaction".to_string(),
command: None,
comment: "Access to transactions".to_string(),
subcommands: vec![CliTransactionList::node(), CliTransactionCreate::node()],
arguments: vec![],
},
name: "account".to_string(),
comment: "Access to accounts".to_string(),
subcommands: vec![
CliAccountList::node(),
CliAccountBalance::node(),
CliAccountCreate::node(),
],
name: "commodity".to_string(),
comment: "Access to commodities".to_string(),
subcommands: vec![CliCommodityList::node(), CliCommodityCreate::node()],
name: "config".to_string(),
comment: "Access to configuration".to_string(),
subcommands: vec![CliGetConfig::node(), CliSetConfig::node()],
name: "sql".to_string(),
comment: "Access to SQL database".to_string(),
subcommands: vec![CliSelectColumn::node()],
name: "reports".to_string(),
comment: "Text-rendered report charts".to_string(),
CliReportsBalance::node(),
CliReportsActivity::node(),
CliReportsBreakdown::node(),
name: "ssh-key".to_string(),
comment: "Manage SSH public keys for remote TUI access".to_string(),
CliSshKeyAdd::node(),
CliSshKeyList::node(),
CliSshKeyRemove::node(),
]
}
/// Walk a command path (`["reports", "balance"]`) down the tree and return
/// the matching leaf. Returns `None` when any segment is unknown or when
/// the path does not terminate at a runnable command.
pub fn find_leaf<'a>(tree: &'a [CommandNode], path: &[&str]) -> Option<&'a CommandNode> {
let (head, rest) = path.split_first()?;
let node = tree.iter().find(|n| n.name == *head)?;
if rest.is_empty() {
node.command.as_ref().map(|_| node)
} else {
find_leaf(&node.subcommands, rest)
#[cfg(test)]
mod tests {
use super::*;
fn all_leaf_paths<'a>(
tree: &'a [CommandNode],
prefix: &[&'a str],
out: &mut Vec<Vec<&'a str>>,
) {
for node in tree {
let mut here: Vec<&'a str> = prefix.to_vec();
here.push(node.name.as_str());
if node.command.is_some() {
out.push(here.clone());
all_leaf_paths(&node.subcommands, &here, out);
#[test]
fn tree_exposes_expected_top_level_groups() {
let tree = command_tree();
let names: Vec<&str> = tree.iter().map(|n| n.name.as_str()).collect();
assert!(names.contains(&"version"));
assert!(names.contains(&"transaction"));
assert!(names.contains(&"account"));
assert!(names.contains(&"commodity"));
assert!(names.contains(&"config"));
assert!(names.contains(&"sql"));
assert!(names.contains(&"reports"));
fn every_leaf_resolves_via_find_leaf() {
let mut leaves = Vec::new();
all_leaf_paths(&tree, &[], &mut leaves);
assert!(!leaves.is_empty());
for path in leaves {
let found = find_leaf(&tree, &path);
assert!(
found.is_some(),
"leaf {path:?} should be resolvable via find_leaf"
);
assert!(found.unwrap().command.is_some());
fn find_leaf_returns_none_for_unknown_path() {
assert!(find_leaf(&tree, &["does-not-exist"]).is_none());
assert!(find_leaf(&tree, &["reports", "nonsense"]).is_none());
fn find_leaf_returns_none_for_group_without_command() {
assert!(find_leaf(&tree, &["reports"]).is_none());
assert!(find_leaf(&tree, &["account"]).is_none());
fn reports_leaves_are_present() {
assert!(find_leaf(&tree, &["reports", "balance"]).is_some());
assert!(find_leaf(&tree, &["reports", "activity"]).is_some());
assert!(find_leaf(&tree, &["reports", "breakdown"]).is_some());