1use cli_core::{CommandNode, find_leaf};
17
18#[derive(Debug, PartialEq, Eq)]
19pub struct PaletteQuery {
20 pub path: Vec<String>,
21 pub args: Vec<(String, String)>,
22}
23
24#[must_use]
26pub fn parse(input: &str) -> PaletteQuery {
27 let mut path = Vec::new();
28 let mut args = Vec::new();
29 for token in input.split_whitespace() {
30 if let Some((k, v)) = token.split_once('=') {
31 args.push((k.to_string(), v.to_string()));
32 } else {
33 path.push(token.to_string());
34 }
35 }
36 PaletteQuery { path, args }
37}
38
39#[must_use]
43pub fn resolve<'a>(tree: &'a [CommandNode], query: &PaletteQuery) -> Option<&'a CommandNode> {
44 let path_refs: Vec<&str> = query.path.iter().map(String::as_str).collect();
45 find_leaf(tree, &path_refs)
46}
47
48#[must_use]
51pub fn longest_common_prefix(candidates: &[String]) -> String {
52 match candidates {
53 [] => String::new(),
54 [one] => one.clone(),
55 [first, rest @ ..] => {
56 let len = rest.iter().fold(first.len(), |n, s| {
57 first
58 .bytes()
59 .zip(s.bytes())
60 .take_while(|(a, b)| a == b)
61 .count()
62 .min(n)
63 });
64 first[..len].to_string()
65 }
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use cli_core::command_tree;
73
74 #[test]
75 fn parses_empty_input_to_empty_query() {
76 let q = parse("");
77 assert!(q.path.is_empty());
78 assert!(q.args.is_empty());
79 }
80
81 #[test]
82 fn parses_single_word_path() {
83 let q = parse("version");
84 assert_eq!(q.path, vec!["version"]);
85 assert!(q.args.is_empty());
86 }
87
88 #[test]
89 fn parses_nested_path() {
90 let q = parse("reports balance");
91 assert_eq!(q.path, vec!["reports", "balance"]);
92 }
93
94 #[test]
95 fn parses_key_value_arguments() {
96 let q = parse("reports balance from=2026-01-01 to=2026-04-30 chart=bar");
97 assert_eq!(q.path, vec!["reports", "balance"]);
98 assert_eq!(q.args.len(), 3);
99 assert_eq!(q.args[0], ("from".to_string(), "2026-01-01".to_string()));
100 assert_eq!(q.args[2], ("chart".to_string(), "bar".to_string()));
101 }
102
103 #[test]
104 fn resolves_version_leaf() {
105 let tree = command_tree();
106 let q = parse("version");
107 let leaf = resolve(&tree, &q).expect("version resolves");
108 assert_eq!(leaf.name, "version");
109 assert!(leaf.is_leaf);
110 }
111
112 #[test]
113 fn resolves_reports_balance_leaf() {
114 let tree = command_tree();
115 let q = parse("reports balance from=2026-01-01");
116 let leaf = resolve(&tree, &q).expect("reports balance resolves");
117 assert_eq!(leaf.name, "balance");
118 assert!(leaf.is_leaf);
119 }
120
121 #[test]
122 fn rejects_unknown_path() {
123 let tree = command_tree();
124 let q = parse("nope");
125 assert!(resolve(&tree, &q).is_none());
126 }
127
128 #[test]
129 fn rejects_group_without_command() {
130 let tree = command_tree();
131 let q = parse("reports");
132 assert!(resolve(&tree, &q).is_none());
133 }
134
135 #[test]
136 fn lcp_empty_candidates_returns_empty() {
137 assert_eq!(longest_common_prefix(&[]), "");
138 }
139
140 #[test]
141 fn lcp_single_candidate_returns_it() {
142 assert_eq!(longest_common_prefix(&["account".to_string()]), "account");
143 }
144
145 #[test]
146 fn lcp_shared_prefix() {
147 let cands = vec!["create".to_string(), "commodity".to_string()];
148 assert_eq!(longest_common_prefix(&cands), "c");
149 }
150
151 #[test]
152 fn lcp_no_shared_prefix() {
153 let cands = vec!["account".to_string(), "transaction".to_string()];
154 assert_eq!(longest_common_prefix(&cands), "");
155 }
156
157 #[test]
158 fn lcp_identical_candidates() {
159 let cands = vec!["list".to_string(), "list".to_string()];
160 assert_eq!(longest_common_prefix(&cands), "list");
161 }
162}