baobao_codegen/commands.rs
1//! Command tree traversal utilities.
2//!
3//! This module provides the [`CommandTree`] abstraction for traversing and
4//! querying commands defined in a schema.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use baobao_codegen::CommandTree;
10//!
11//! let tree = CommandTree::new(&schema);
12//!
13//! // Iterate all commands
14//! for cmd in tree.iter() {
15//! println!("{}", cmd.path_str("/"));
16//! }
17//!
18//! // Get only leaf commands (handlers)
19//! for cmd in tree.leaves() {
20//! println!("handler: {}", cmd.path_str("/"));
21//! }
22//!
23//! // Get only parent commands (subcommand groups)
24//! for cmd in tree.parents() {
25//! println!("group: {}", cmd.path_str("/"));
26//! }
27//! ```
28
29use baobao_manifest::{Command, Manifest};
30
31/// A traversable view of the command tree in a schema.
32///
33/// `CommandTree` provides a unified API for iterating over commands,
34/// filtering by type (leaf vs parent), and accessing command metadata.
35#[derive(Debug, Clone)]
36pub struct CommandTree<'a> {
37 commands: Vec<FlatCommand<'a>>,
38}
39
40impl<'a> CommandTree<'a> {
41 /// Create a new CommandTree from a schema.
42 pub fn new(schema: &'a Manifest) -> Self {
43 let mut commands = Vec::new();
44 Self::flatten_recursive(&schema.commands, Vec::new(), 0, &mut commands);
45 Self { commands }
46 }
47
48 fn flatten_recursive(
49 commands: &'a std::collections::HashMap<String, Command>,
50 parent_path: Vec<&'a str>,
51 depth: usize,
52 result: &mut Vec<FlatCommand<'a>>,
53 ) {
54 for (name, command) in commands {
55 let mut path = parent_path.clone();
56 path.push(name.as_str());
57
58 let is_leaf = !command.has_subcommands();
59
60 result.push(FlatCommand {
61 name: name.as_str(),
62 path: path.clone(),
63 depth,
64 is_leaf,
65 command,
66 });
67
68 if command.has_subcommands() {
69 Self::flatten_recursive(&command.commands, path, depth + 1, result);
70 }
71 }
72 }
73
74 /// Iterate over all commands in depth-first order.
75 pub fn iter(&self) -> impl Iterator<Item = &FlatCommand<'a>> {
76 self.commands.iter()
77 }
78
79 /// Iterate over leaf commands only (commands without subcommands).
80 ///
81 /// These are the commands that have handlers.
82 pub fn leaves(&self) -> impl Iterator<Item = &FlatCommand<'a>> {
83 self.commands.iter().filter(|cmd| cmd.is_leaf)
84 }
85
86 /// Iterate over parent commands only (commands with subcommands).
87 ///
88 /// These are command groups that contain other commands.
89 pub fn parents(&self) -> impl Iterator<Item = &FlatCommand<'a>> {
90 self.commands.iter().filter(|cmd| !cmd.is_leaf)
91 }
92
93 /// Get the total number of commands.
94 pub fn len(&self) -> usize {
95 self.commands.len()
96 }
97
98 /// Check if the tree is empty.
99 pub fn is_empty(&self) -> bool {
100 self.commands.is_empty()
101 }
102
103 /// Get the number of leaf commands.
104 pub fn leaf_count(&self) -> usize {
105 self.commands.iter().filter(|cmd| cmd.is_leaf).count()
106 }
107
108 /// Get the number of parent commands.
109 pub fn parent_count(&self) -> usize {
110 self.commands.iter().filter(|cmd| !cmd.is_leaf).count()
111 }
112
113 /// Convert to a Vec of all commands.
114 pub fn to_vec(&self) -> Vec<FlatCommand<'a>> {
115 self.commands.clone()
116 }
117
118 /// Collect all command paths as strings.
119 ///
120 /// Returns a set of path strings like "db/migrate", "hello".
121 pub fn collect_paths(&self) -> std::collections::HashSet<String> {
122 self.iter().map(|cmd| cmd.path_str("/")).collect()
123 }
124
125 /// Collect only leaf command paths (commands without subcommands).
126 ///
127 /// These are the paths that correspond to actual handler files.
128 pub fn collect_leaf_paths(&self) -> std::collections::HashSet<String> {
129 self.leaves().map(|cmd| cmd.path_str("/")).collect()
130 }
131}
132
133impl<'a> IntoIterator for CommandTree<'a> {
134 type Item = FlatCommand<'a>;
135 type IntoIter = std::vec::IntoIter<FlatCommand<'a>>;
136
137 fn into_iter(self) -> Self::IntoIter {
138 self.commands.into_iter()
139 }
140}
141
142impl<'a> IntoIterator for &'a CommandTree<'a> {
143 type Item = &'a FlatCommand<'a>;
144 type IntoIter = std::slice::Iter<'a, FlatCommand<'a>>;
145
146 fn into_iter(self) -> Self::IntoIter {
147 self.commands.iter()
148 }
149}
150
151/// Flattened command info for easier processing.
152///
153/// Instead of recursively traversing the command tree, you can get
154/// a flat list of all commands with their paths.
155#[derive(Debug, Clone)]
156pub struct FlatCommand<'a> {
157 /// Command name (e.g., "migrate")
158 pub name: &'a str,
159 /// Full path segments (e.g., ["db", "migrate"])
160 pub path: Vec<&'a str>,
161 /// Depth in the command tree (0 = top-level)
162 pub depth: usize,
163 /// Whether this is a leaf command (no subcommands)
164 pub is_leaf: bool,
165 /// Reference to the command definition
166 pub command: &'a Command,
167}
168
169impl<'a> FlatCommand<'a> {
170 /// Get the full path as a string with the given separator.
171 ///
172 /// # Example
173 ///
174 /// ```ignore
175 /// let cmd = FlatCommand { path: vec!["db", "migrate"], .. };
176 /// assert_eq!(cmd.path_str("/"), "db/migrate");
177 /// assert_eq!(cmd.path_str("::"), "db::migrate");
178 /// ```
179 pub fn path_str(&self, sep: &str) -> String {
180 self.path.join(sep)
181 }
182
183 /// Get the parent path (excluding this command's name).
184 pub fn parent_path(&self) -> Vec<&'a str> {
185 if self.path.len() > 1 {
186 self.path[..self.path.len() - 1].to_vec()
187 } else {
188 Vec::new()
189 }
190 }
191}