baobao_codegen/commands.rs
1//! Command tree traversal utilities.
2
3use baobao_manifest::{Command, Schema};
4
5/// Flattened command info for easier processing.
6///
7/// Instead of recursively traversing the command tree, you can get
8/// a flat list of all commands with their paths.
9#[derive(Debug, Clone)]
10pub struct FlatCommand<'a> {
11 /// Command name (e.g., "migrate")
12 pub name: &'a str,
13 /// Full path segments (e.g., ["db", "migrate"])
14 pub path: Vec<&'a str>,
15 /// Depth in the command tree (0 = top-level)
16 pub depth: usize,
17 /// Whether this is a leaf command (no subcommands)
18 pub is_leaf: bool,
19 /// Reference to the command definition
20 pub command: &'a Command,
21}
22
23impl<'a> FlatCommand<'a> {
24 /// Get the full path as a string with the given separator.
25 ///
26 /// # Example
27 ///
28 /// ```ignore
29 /// let cmd = FlatCommand { path: vec!["db", "migrate"], .. };
30 /// assert_eq!(cmd.path_str("/"), "db/migrate");
31 /// assert_eq!(cmd.path_str("::"), "db::migrate");
32 /// ```
33 pub fn path_str(&self, sep: &str) -> String {
34 self.path.join(sep)
35 }
36
37 /// Get the parent path (excluding this command's name).
38 pub fn parent_path(&self) -> Vec<&'a str> {
39 if self.path.len() > 1 {
40 self.path[..self.path.len() - 1].to_vec()
41 } else {
42 Vec::new()
43 }
44 }
45}
46
47/// Walk all commands in a schema and return a flat list.
48///
49/// Commands are returned in depth-first order.
50///
51/// # Example
52///
53/// ```ignore
54/// let commands = flatten_commands(&schema);
55/// for cmd in commands {
56/// println!("{}: leaf={}", cmd.path_str("/"), cmd.is_leaf);
57/// }
58/// ```
59pub fn flatten_commands(schema: &Schema) -> Vec<FlatCommand<'_>> {
60 let mut result = Vec::new();
61 flatten_commands_recursive(&schema.commands, Vec::new(), 0, &mut result);
62 result
63}
64
65fn flatten_commands_recursive<'a>(
66 commands: &'a std::collections::HashMap<String, Command>,
67 parent_path: Vec<&'a str>,
68 depth: usize,
69 result: &mut Vec<FlatCommand<'a>>,
70) {
71 for (name, command) in commands {
72 let mut path = parent_path.clone();
73 path.push(name.as_str());
74
75 let is_leaf = !command.has_subcommands();
76
77 result.push(FlatCommand {
78 name: name.as_str(),
79 path: path.clone(),
80 depth,
81 is_leaf,
82 command,
83 });
84
85 if command.has_subcommands() {
86 flatten_commands_recursive(&command.commands, path, depth + 1, result);
87 }
88 }
89}
90
91/// Visitor trait for command tree traversal.
92///
93/// Implement this trait to process commands without manual recursion.
94pub trait CommandVisitor<'a> {
95 /// Called for each command in the tree.
96 fn visit(&mut self, cmd: &FlatCommand<'a>);
97}
98
99/// Walk all commands with a visitor.
100///
101/// # Example
102///
103/// ```ignore
104/// struct LeafCollector {
105/// leaves: Vec<String>,
106/// }
107///
108/// impl CommandVisitor<'_> for LeafCollector {
109/// fn visit(&mut self, cmd: &FlatCommand) {
110/// if cmd.is_leaf {
111/// self.leaves.push(cmd.path_str("/"));
112/// }
113/// }
114/// }
115///
116/// let mut collector = LeafCollector { leaves: vec![] };
117/// walk_commands(&schema, &mut collector);
118/// ```
119pub fn walk_commands<'a, V: CommandVisitor<'a>>(schema: &'a Schema, visitor: &mut V) {
120 let commands = flatten_commands(schema);
121 for cmd in &commands {
122 visitor.visit(cmd);
123 }
124}
125
126/// Get only leaf commands (commands without subcommands).
127pub fn leaf_commands(schema: &Schema) -> Vec<FlatCommand<'_>> {
128 flatten_commands(schema)
129 .into_iter()
130 .filter(|cmd| cmd.is_leaf)
131 .collect()
132}
133
134/// Get only parent commands (commands with subcommands).
135pub fn parent_commands(schema: &Schema) -> Vec<FlatCommand<'_>> {
136 flatten_commands(schema)
137 .into_iter()
138 .filter(|cmd| !cmd.is_leaf)
139 .collect()
140}
141
142#[cfg(test)]
143mod tests {
144 // Note: These tests would require constructing a Schema,
145 // which depends on bao-schema. In practice, you'd test this
146 // in an integration test or with a test helper.
147
148 #[test]
149 fn test_flat_command_path_str() {
150 // Minimal test without Schema dependency
151 let path = ["db", "migrate"];
152 assert_eq!(path.join("/"), "db/migrate");
153 assert_eq!(path.join("::"), "db::migrate");
154 }
155}