1use crate::node::CommandNode;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum NodeRef {
13 Command(Vec<String>),
15 Flag {
17 path: Vec<String>,
19 key: FlagKey,
21 },
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub enum FlagKey {
27 Long(String),
29 Short(char),
31}
32
33pub fn resolve<'a>(root: &'a CommandNode, path: &[String]) -> Option<&'a CommandNode> {
42 let mut segments = path.iter();
43 let first = segments.next()?;
44 if !names_match(root, first) {
45 return None;
46 }
47 let mut current = root;
48 for seg in segments {
49 current = current.subcommands.iter().find(|c| names_match(c, seg))?;
50 }
51 Some(current)
52}
53
54pub fn resolve_mut<'a>(root: &'a mut CommandNode, path: &[String]) -> Option<&'a mut CommandNode> {
57 let mut segments = path.iter();
58 let first = segments.next()?;
59 if !names_match(root, first) {
60 return None;
61 }
62 let mut current = root;
63 for seg in segments {
64 current = current
65 .subcommands
66 .iter_mut()
67 .find(|c| names_match(c, seg))?;
68 }
69 Some(current)
70}
71
72fn names_match(node: &CommandNode, segment: &str) -> bool {
73 node.name == segment || node.aliases.iter().any(|a| a == segment)
74}
75
76pub fn resolve_flag<'a>(
78 root: &'a CommandNode,
79 path: &[String],
80 key: &FlagKey,
81) -> Option<&'a crate::node::Flag> {
82 let node = resolve(root, path)?;
83 node.flags.iter().find(|f| f.matches_key(key))
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::provenance::{Provenance, Source};
90
91 fn leaf(name: &str) -> CommandNode {
92 CommandNode::new(name, Provenance::single(Source::HelpText))
93 }
94
95 #[test]
96 fn resolves_root() {
97 let root = leaf("git");
98 let found = resolve(&root, &["git".to_string()]).unwrap();
99 assert_eq!(found.name, "git");
100 }
101
102 #[test]
103 fn resolves_nested_path() {
104 let mut root = leaf("git");
105 let mut rebase = leaf("rebase");
106 rebase.subcommands.push(leaf("--onto-helper"));
107 root.subcommands.push(rebase);
108 let path = vec!["git".to_string(), "rebase".to_string()];
109 let found = resolve(&root, &path).unwrap();
110 assert_eq!(found.name, "rebase");
111 }
112
113 #[test]
114 fn returns_none_for_unknown_segment() {
115 let root = leaf("git");
116 let path = vec!["git".to_string(), "nonexistent".to_string()];
117 assert!(resolve(&root, &path).is_none());
118 }
119
120 #[test]
127 fn subcommand_sharing_parent_name_resolves_correctly() {
128 let mut inner_a = leaf("a");
132 inner_a.subcommands.push(leaf("b"));
133 inner_a.subcommands[0].summary = None;
136 let mut outer_a = leaf("a");
137 outer_a.subcommands.push(inner_a);
138 let path = vec!["a".to_string(), "a".to_string(), "b".to_string()];
140 let found = resolve(&outer_a, &path).expect("must resolve through nested same-named node");
141 assert_eq!(found.name, "b");
142
143 let bad_path = vec!["a".to_string(), "b".to_string()];
146 assert!(resolve(&outer_a, &bad_path).is_none());
147 }
148
149 #[test]
150 fn resolves_via_alias() {
151 let mut root = leaf("git");
152 let mut add = leaf("add");
153 add.aliases.push("stage".to_string());
154 root.subcommands.push(add);
155 let path = vec!["git".to_string(), "stage".to_string()];
156 let found = resolve(&root, &path).unwrap();
157 assert_eq!(found.name, "add");
158 }
159
160 #[test]
161 fn resolve_mut_allows_splicing() {
162 let mut root = leaf("git");
163 root.subcommands.push(leaf("rebase"));
164 {
165 let node = resolve_mut(&mut root, &["git".to_string(), "rebase".to_string()]).unwrap();
166 node.children_filled = true;
167 }
168 assert!(root.subcommands[0].children_filled);
169 }
170}