1use crate::commands::{
2 CliAccountBalance, CliAccountCreate, CliAccountList, CliCommand, CliCommodityCreate,
3 CliCommodityList, CliGetConfig, CliReportsActivity, CliReportsBalance, CliReportsBreakdown,
4 CliSelectColumn, CliSetConfig, CliSshKeyAdd, CliSshKeyList, CliSshKeyRemove,
5 CliTransactionCreate, CliTransactionList, CliVersion, CommandNode,
6};
7
8#[must_use]
12pub fn command_tree() -> Vec<CommandNode> {
13 vec![
14 CliVersion::node(),
15 CommandNode {
16 name: "transaction".to_string(),
17 command: None,
18 comment: "Access to transactions".to_string(),
19 subcommands: vec![CliTransactionList::node(), CliTransactionCreate::node()],
20 arguments: vec![],
21 },
22 CommandNode {
23 name: "account".to_string(),
24 command: None,
25 comment: "Access to accounts".to_string(),
26 subcommands: vec![
27 CliAccountList::node(),
28 CliAccountBalance::node(),
29 CliAccountCreate::node(),
30 ],
31 arguments: vec![],
32 },
33 CommandNode {
34 name: "commodity".to_string(),
35 command: None,
36 comment: "Access to commodities".to_string(),
37 subcommands: vec![CliCommodityList::node(), CliCommodityCreate::node()],
38 arguments: vec![],
39 },
40 CommandNode {
41 name: "config".to_string(),
42 command: None,
43 comment: "Access to configuration".to_string(),
44 subcommands: vec![CliGetConfig::node(), CliSetConfig::node()],
45 arguments: vec![],
46 },
47 CommandNode {
48 name: "sql".to_string(),
49 command: None,
50 comment: "Access to SQL database".to_string(),
51 subcommands: vec![CliSelectColumn::node()],
52 arguments: vec![],
53 },
54 CommandNode {
55 name: "reports".to_string(),
56 command: None,
57 comment: "Text-rendered report charts".to_string(),
58 subcommands: vec![
59 CliReportsBalance::node(),
60 CliReportsActivity::node(),
61 CliReportsBreakdown::node(),
62 ],
63 arguments: vec![],
64 },
65 CommandNode {
66 name: "ssh-key".to_string(),
67 command: None,
68 comment: "Manage SSH public keys for remote TUI access".to_string(),
69 subcommands: vec![
70 CliSshKeyAdd::node(),
71 CliSshKeyList::node(),
72 CliSshKeyRemove::node(),
73 ],
74 arguments: vec![],
75 },
76 ]
77}
78
79#[must_use]
83pub fn find_leaf<'a>(tree: &'a [CommandNode], path: &[&str]) -> Option<&'a CommandNode> {
84 let (head, rest) = path.split_first()?;
85 let node = tree.iter().find(|n| n.name == *head)?;
86 if rest.is_empty() {
87 node.command.as_ref().map(|_| node)
88 } else {
89 find_leaf(&node.subcommands, rest)
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 fn all_leaf_paths<'a>(
98 tree: &'a [CommandNode],
99 prefix: &[&'a str],
100 out: &mut Vec<Vec<&'a str>>,
101 ) {
102 for node in tree {
103 let mut here: Vec<&'a str> = prefix.to_vec();
104 here.push(node.name.as_str());
105 if node.command.is_some() {
106 out.push(here.clone());
107 }
108 all_leaf_paths(&node.subcommands, &here, out);
109 }
110 }
111
112 #[test]
113 fn tree_exposes_expected_top_level_groups() {
114 let tree = command_tree();
115 let names: Vec<&str> = tree.iter().map(|n| n.name.as_str()).collect();
116 assert!(names.contains(&"version"));
117 assert!(names.contains(&"transaction"));
118 assert!(names.contains(&"account"));
119 assert!(names.contains(&"commodity"));
120 assert!(names.contains(&"config"));
121 assert!(names.contains(&"sql"));
122 assert!(names.contains(&"reports"));
123 }
124
125 #[test]
126 fn every_leaf_resolves_via_find_leaf() {
127 let tree = command_tree();
128 let mut leaves = Vec::new();
129 all_leaf_paths(&tree, &[], &mut leaves);
130 assert!(!leaves.is_empty());
131 for path in leaves {
132 let found = find_leaf(&tree, &path);
133 assert!(
134 found.is_some(),
135 "leaf {path:?} should be resolvable via find_leaf"
136 );
137 assert!(found.unwrap().command.is_some());
138 }
139 }
140
141 #[test]
142 fn find_leaf_returns_none_for_unknown_path() {
143 let tree = command_tree();
144 assert!(find_leaf(&tree, &["does-not-exist"]).is_none());
145 assert!(find_leaf(&tree, &["reports", "nonsense"]).is_none());
146 }
147
148 #[test]
149 fn find_leaf_returns_none_for_group_without_command() {
150 let tree = command_tree();
151 assert!(find_leaf(&tree, &["reports"]).is_none());
152 assert!(find_leaf(&tree, &["account"]).is_none());
153 }
154
155 #[test]
156 fn reports_leaves_are_present() {
157 let tree = command_tree();
158 assert!(find_leaf(&tree, &["reports", "balance"]).is_some());
159 assert!(find_leaf(&tree, &["reports", "activity"]).is_some());
160 assert!(find_leaf(&tree, &["reports", "breakdown"]).is_some());
161 }
162}