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}