osp_cli/completion/
tree.rs1use std::collections::BTreeMap;
2
3use crate::completion::model::{
4 ArgNode, CompletionNode, CompletionTree, FlagNode, SuggestionEntry,
5};
6
7#[derive(Debug, Clone, Default, PartialEq, Eq)]
8pub struct CommandSpec {
9 pub name: String,
11 pub tooltip: Option<String>,
12 pub sort: Option<String>,
13 pub args: Vec<ArgNode>,
14 pub flags: BTreeMap<String, FlagNode>,
15 pub subcommands: Vec<CommandSpec>,
16}
17
18impl CommandSpec {
19 pub fn new(name: impl Into<String>) -> Self {
20 Self {
21 name: name.into(),
22 ..Self::default()
23 }
24 }
25
26 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
27 self.tooltip = Some(tooltip.into());
28 self
29 }
30
31 pub fn sort(mut self, sort: impl Into<String>) -> Self {
32 self.sort = Some(sort.into());
33 self
34 }
35
36 pub fn arg(mut self, arg: ArgNode) -> Self {
37 self.args.push(arg);
38 self
39 }
40
41 pub fn args(mut self, args: impl IntoIterator<Item = ArgNode>) -> Self {
42 self.args.extend(args);
43 self
44 }
45
46 pub fn flag(mut self, name: impl Into<String>, flag: FlagNode) -> Self {
47 self.flags.insert(name.into(), flag);
48 self
49 }
50
51 pub fn flags(mut self, flags: impl IntoIterator<Item = (String, FlagNode)>) -> Self {
52 self.flags.extend(flags);
53 self
54 }
55
56 pub fn subcommand(mut self, subcommand: CommandSpec) -> Self {
57 self.subcommands.push(subcommand);
58 self
59 }
60
61 pub fn subcommands(mut self, subcommands: impl IntoIterator<Item = CommandSpec>) -> Self {
62 self.subcommands.extend(subcommands);
63 self
64 }
65}
66
67#[derive(Debug, Clone, Default)]
68pub struct CompletionTreeBuilder;
69
70impl CompletionTreeBuilder {
71 pub fn build_from_specs(
77 &self,
78 specs: &[CommandSpec],
79 pipe_verbs: impl IntoIterator<Item = (String, String)>,
80 ) -> CompletionTree {
81 let mut root = CompletionNode::default();
82 for spec in specs {
83 let name = spec.name.clone();
84 assert!(
85 root.children
86 .insert(name.clone(), Self::node_from_spec(spec))
87 .is_none(),
88 "duplicate root command spec: {name}"
89 );
90 }
91
92 CompletionTree {
93 root,
94 pipe_verbs: pipe_verbs.into_iter().collect(),
95 }
96 }
97
98 pub fn apply_config_set_keys(
99 &self,
100 tree: &mut CompletionTree,
101 keys: impl IntoIterator<Item = ConfigKeySpec>,
102 ) {
103 let Some(config_node) = tree.root.children.get_mut("config") else {
104 return;
105 };
106 let Some(set_node) = config_node.children.get_mut("set") else {
107 return;
108 };
109
110 for key in keys {
111 let key_name = key.key.clone();
112 let mut node = CompletionNode {
113 tooltip: key.tooltip,
114 value_key: true,
115 ..CompletionNode::default()
116 };
117 for suggestion in key.value_suggestions {
118 node.children.insert(
119 suggestion.value.clone(),
120 CompletionNode {
121 value_leaf: true,
122 tooltip: suggestion.meta.clone(),
123 ..CompletionNode::default()
124 },
125 );
126 }
127 assert!(
128 set_node.children.insert(key_name.clone(), node).is_none(),
129 "duplicate config set key: {key_name}"
130 );
131 }
132 }
133
134 fn node_from_spec(spec: &CommandSpec) -> CompletionNode {
135 let mut node = CompletionNode {
136 tooltip: spec.tooltip.clone(),
137 sort: spec.sort.clone(),
138 args: spec.args.clone(),
139 flags: spec.flags.clone(),
140 ..CompletionNode::default()
141 };
142
143 for subcommand in &spec.subcommands {
144 let name = subcommand.name.clone();
145 assert!(
146 node.children
147 .insert(name.clone(), Self::node_from_spec(subcommand))
148 .is_none(),
149 "duplicate subcommand spec: {name}"
150 );
151 }
152
153 node
154 }
155}
156
157#[derive(Debug, Clone, Default, PartialEq, Eq)]
158pub struct ConfigKeySpec {
159 pub key: String,
160 pub tooltip: Option<String>,
161 pub value_suggestions: Vec<SuggestionEntry>,
162}
163
164impl ConfigKeySpec {
165 pub fn new(key: impl Into<String>) -> Self {
166 Self {
167 key: key.into(),
168 ..Self::default()
169 }
170 }
171
172 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
173 self.tooltip = Some(tooltip.into());
174 self
175 }
176
177 pub fn value_suggestions(
178 mut self,
179 suggestions: impl IntoIterator<Item = SuggestionEntry>,
180 ) -> Self {
181 self.value_suggestions = suggestions.into_iter().collect();
182 self
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use crate::completion::model::CompletionTree;
189
190 use super::{CommandSpec, CompletionTreeBuilder, ConfigKeySpec};
191
192 fn build_tree() -> CompletionTree {
193 CompletionTreeBuilder.build_from_specs(
194 &[CommandSpec::new("config").subcommand(CommandSpec::new("set"))],
195 [("F".to_string(), "Filter".to_string())],
196 )
197 }
198
199 #[test]
200 fn builds_nested_tree_from_specs() {
201 let tree = build_tree();
202 assert!(tree.root.children.contains_key("config"));
203 assert!(
204 tree.root
205 .children
206 .get("config")
207 .and_then(|node| node.children.get("set"))
208 .is_some()
209 );
210 }
211
212 #[test]
213 fn injects_config_key_nodes() {
214 let mut tree = build_tree();
215 CompletionTreeBuilder.apply_config_set_keys(
216 &mut tree,
217 [
218 ConfigKeySpec::new("ui.format"),
219 ConfigKeySpec::new("log.level"),
220 ],
221 );
222
223 let set_node = &tree.root.children["config"].children["set"];
224 assert!(set_node.children.contains_key("ui.format"));
225 assert!(set_node.children.contains_key("log.level"));
226 assert!(set_node.children["ui.format"].value_key);
227 }
228
229 #[test]
230 #[should_panic(expected = "duplicate root command spec")]
231 fn duplicate_root_specs_fail_fast() {
232 let _ = CompletionTreeBuilder.build_from_specs(
233 &[CommandSpec::new("config"), CommandSpec::new("config")],
234 [],
235 );
236 }
237
238 #[test]
239 #[should_panic(expected = "duplicate config set key")]
240 fn duplicate_config_keys_fail_fast() {
241 let mut tree = build_tree();
242 CompletionTreeBuilder.apply_config_set_keys(
243 &mut tree,
244 [
245 ConfigKeySpec::new("ui.format"),
246 ConfigKeySpec::new("ui.format"),
247 ],
248 );
249 }
250}