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
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 full path with a name transformer applied to each segment.
184    ///
185    /// # Example
186    ///
187    /// ```ignore
188    /// use baobao_core::to_snake_case;
189    /// let cmd = FlatCommand { path: vec!["my-db", "run-migration"], .. };
190    /// assert_eq!(cmd.path_transformed("/", to_snake_case), "my_db/run_migration");
191    /// ```
192    pub fn path_transformed<F>(&self, sep: &str, transform: F) -> String
193    where
194        F: Fn(&str) -> String,
195    {
196        self.path
197            .iter()
198            .map(|s| transform(s))
199            .collect::<Vec<_>>()
200            .join(sep)
201    }
202
203    /// Get the parent path (excluding this command's name).
204    pub fn parent_path(&self) -> Vec<&'a str> {
205        if self.path.len() > 1 {
206            self.path[..self.path.len() - 1].to_vec()
207        } else {
208            Vec::new()
209        }
210    }
211
212    /// Get the directory path for this command's handler file.
213    ///
214    /// For a leaf command at path ["db", "migrate"], returns the path to
215    /// the directory where the handler file should be created.
216    pub fn handler_dir(
217        &self,
218        base: &std::path::Path,
219        transform: impl Fn(&str) -> String,
220    ) -> std::path::PathBuf {
221        let mut dir = base.to_path_buf();
222        // For leaf commands, we want the parent directory
223        // For parent commands, we want the full directory path
224        let segments = if self.is_leaf {
225            &self.path[..self.path.len().saturating_sub(1)]
226        } else {
227            &self.path[..]
228        };
229        for segment in segments {
230            dir.push(transform(segment));
231        }
232        dir
233    }
234
235    /// Get the path segments as owned strings with a transform applied.
236    pub fn path_segments_transformed<F>(&self, transform: F) -> Vec<String>
237    where
238        F: Fn(&str) -> String,
239    {
240        self.path.iter().map(|s| transform(s)).collect()
241    }
242}