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}