baobao_codegen/schema/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
31use super::display::{CommandTreeDisplay, DisplayStyle};
32
33/// A traversable view of the command tree in a schema.
34///
35/// `CommandTree` provides a unified API for iterating over commands,
36/// filtering by type (leaf vs parent), and accessing command metadata.
37#[derive(Debug, Clone)]
38pub struct CommandTree<'a> {
39 commands: Vec<FlatCommand<'a>>,
40}
41
42impl<'a> CommandTree<'a> {
43 /// Create a new CommandTree from a schema.
44 pub fn new(schema: &'a Manifest) -> Self {
45 let mut commands = Vec::new();
46 Self::flatten_recursive(&schema.commands, Vec::new(), 0, &mut commands);
47 Self { commands }
48 }
49
50 fn flatten_recursive(
51 commands: &'a std::collections::HashMap<String, Command>,
52 parent_path: Vec<&'a str>,
53 depth: usize,
54 result: &mut Vec<FlatCommand<'a>>,
55 ) {
56 for (name, command) in commands {
57 let mut path = parent_path.clone();
58 path.push(name.as_str());
59
60 let is_leaf = !command.has_subcommands();
61
62 result.push(FlatCommand {
63 name: name.as_str(),
64 path: path.clone(),
65 depth,
66 is_leaf,
67 command,
68 });
69
70 if command.has_subcommands() {
71 Self::flatten_recursive(&command.commands, path, depth + 1, result);
72 }
73 }
74 }
75
76 /// Iterate over all commands in depth-first order.
77 pub fn iter(&self) -> impl Iterator<Item = &FlatCommand<'a>> {
78 self.commands.iter()
79 }
80
81 /// Iterate over leaf commands only (commands without subcommands).
82 ///
83 /// These are the commands that have handlers.
84 pub fn leaves(&self) -> impl Iterator<Item = &FlatCommand<'a>> {
85 self.commands.iter().filter(|cmd| cmd.is_leaf)
86 }
87
88 /// Iterate over parent commands only (commands with subcommands).
89 ///
90 /// These are command groups that contain other commands.
91 pub fn parents(&self) -> impl Iterator<Item = &FlatCommand<'a>> {
92 self.commands.iter().filter(|cmd| !cmd.is_leaf)
93 }
94
95 /// Get the total number of commands.
96 pub fn len(&self) -> usize {
97 self.commands.len()
98 }
99
100 /// Check if the tree is empty.
101 pub fn is_empty(&self) -> bool {
102 self.commands.is_empty()
103 }
104
105 /// Get the number of leaf commands.
106 pub fn leaf_count(&self) -> usize {
107 self.commands.iter().filter(|cmd| cmd.is_leaf).count()
108 }
109
110 /// Get the number of parent commands.
111 pub fn parent_count(&self) -> usize {
112 self.commands.iter().filter(|cmd| !cmd.is_leaf).count()
113 }
114
115 /// Convert to a Vec of all commands.
116 pub fn to_vec(&self) -> Vec<FlatCommand<'a>> {
117 self.commands.clone()
118 }
119
120 /// Collect all command paths as strings.
121 ///
122 /// Returns a set of path strings like "db/migrate", "hello".
123 pub fn collect_paths(&self) -> std::collections::HashSet<String> {
124 self.iter().map(|cmd| cmd.path_str("/")).collect()
125 }
126
127 /// Collect only leaf command paths (commands without subcommands).
128 ///
129 /// These are the paths that correspond to actual handler files.
130 pub fn collect_leaf_paths(&self) -> std::collections::HashSet<String> {
131 self.leaves().map(|cmd| cmd.path_str("/")).collect()
132 }
133
134 /// Create a display formatter with default settings.
135 pub fn display(&'a self) -> CommandTreeDisplay<'a> {
136 CommandTreeDisplay::new(self)
137 }
138
139 /// Create a display formatter with a specific style.
140 pub fn display_style(&'a self, style: DisplayStyle) -> CommandTreeDisplay<'a> {
141 CommandTreeDisplay::new(self).style(style)
142 }
143}
144
145impl<'a> IntoIterator for CommandTree<'a> {
146 type Item = FlatCommand<'a>;
147 type IntoIter = std::vec::IntoIter<FlatCommand<'a>>;
148
149 fn into_iter(self) -> Self::IntoIter {
150 self.commands.into_iter()
151 }
152}
153
154impl<'a> IntoIterator for &'a CommandTree<'a> {
155 type Item = &'a FlatCommand<'a>;
156 type IntoIter = std::slice::Iter<'a, FlatCommand<'a>>;
157
158 fn into_iter(self) -> Self::IntoIter {
159 self.commands.iter()
160 }
161}
162
163/// Flattened command info for easier processing.
164///
165/// Instead of recursively traversing the command tree, you can get
166/// a flat list of all commands with their paths.
167#[derive(Debug, Clone)]
168pub struct FlatCommand<'a> {
169 /// Command name (e.g., "migrate")
170 pub name: &'a str,
171 /// Full path segments (e.g., ["db", "migrate"])
172 pub path: Vec<&'a str>,
173 /// Depth in the command tree (0 = top-level)
174 pub depth: usize,
175 /// Whether this is a leaf command (no subcommands)
176 pub is_leaf: bool,
177 /// Reference to the command definition
178 pub command: &'a Command,
179}
180
181impl<'a> FlatCommand<'a> {
182 /// Get the full path as a string with the given separator.
183 ///
184 /// # Example
185 ///
186 /// ```ignore
187 /// let cmd = FlatCommand { path: vec!["db", "migrate"], .. };
188 /// assert_eq!(cmd.path_str("/"), "db/migrate");
189 /// assert_eq!(cmd.path_str("::"), "db::migrate");
190 /// ```
191 pub fn path_str(&self, sep: &str) -> String {
192 self.path.join(sep)
193 }
194
195 /// Get the full path with a name transformer applied to each segment.
196 ///
197 /// # Example
198 ///
199 /// ```ignore
200 /// use baobao_core::to_snake_case;
201 /// let cmd = FlatCommand { path: vec!["my-db", "run-migration"], .. };
202 /// assert_eq!(cmd.path_transformed("/", to_snake_case), "my_db/run_migration");
203 /// ```
204 pub fn path_transformed<F>(&self, sep: &str, transform: F) -> String
205 where
206 F: Fn(&str) -> String,
207 {
208 self.path
209 .iter()
210 .map(|s| transform(s))
211 .collect::<Vec<_>>()
212 .join(sep)
213 }
214
215 /// Get the parent path (excluding this command's name).
216 pub fn parent_path(&self) -> Vec<&'a str> {
217 if self.path.len() > 1 {
218 self.path[..self.path.len() - 1].to_vec()
219 } else {
220 Vec::new()
221 }
222 }
223
224 /// Get the directory path for this command's handler file.
225 ///
226 /// For a leaf command at path ["db", "migrate"], returns the path to
227 /// the directory where the handler file should be created.
228 pub fn handler_dir(
229 &self,
230 base: &std::path::Path,
231 transform: impl Fn(&str) -> String,
232 ) -> std::path::PathBuf {
233 let mut dir = base.to_path_buf();
234 // For leaf commands, we want the parent directory
235 // For parent commands, we want the full directory path
236 let segments = if self.is_leaf {
237 &self.path[..self.path.len().saturating_sub(1)]
238 } else {
239 &self.path[..]
240 };
241 for segment in segments {
242 dir.push(transform(segment));
243 }
244 dir
245 }
246
247 /// Get the path segments as owned strings with a transform applied.
248 pub fn path_segments_transformed<F>(&self, transform: F) -> Vec<String>
249 where
250 F: Fn(&str) -> String,
251 {
252 self.path.iter().map(|s| transform(s)).collect()
253 }
254}