Skip to main content

osp_cli/completion/
tree.rs

1use 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    /// Declarative command description used to build the plain completion tree.
10    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    /// Build the immutable completion tree from higher-level command specs.
72    ///
73    /// The resulting structure is intentionally plain data so callers can cache
74    /// it, augment it with plugin/provider hints, and pass it into the engine
75    /// without keeping builder state alive.
76    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}