baobao_codegen/
handlers.rs

1//! Handler file path management utilities.
2
3use std::{
4    collections::HashSet,
5    path::{Path, PathBuf},
6};
7
8use baobao_manifest::Schema;
9use eyre::Result;
10
11use crate::commands::flatten_commands;
12
13/// Manages handler file paths for a code generator.
14///
15/// Provides utilities for:
16/// - Computing handler file paths from command paths
17/// - Finding orphaned handler files
18/// - Checking if handlers exist
19#[derive(Debug, Clone)]
20pub struct HandlerPaths {
21    /// Base directory for handlers (e.g., "src/handlers")
22    base_dir: PathBuf,
23    /// File extension without dot (e.g., "rs", "ts")
24    extension: String,
25}
26
27impl HandlerPaths {
28    /// Create a new HandlerPaths instance.
29    pub fn new(base_dir: impl Into<PathBuf>, extension: impl Into<String>) -> Self {
30        Self {
31            base_dir: base_dir.into(),
32            extension: extension.into(),
33        }
34    }
35
36    /// Get the handler file path for a command.
37    ///
38    /// # Example
39    ///
40    /// ```ignore
41    /// let paths = HandlerPaths::new("src/handlers", "rs");
42    /// assert_eq!(
43    ///     paths.handler_path(&["db", "migrate"]),
44    ///     PathBuf::from("src/handlers/db/migrate.rs")
45    /// );
46    /// ```
47    pub fn handler_path(&self, command_path: &[&str]) -> PathBuf {
48        let mut path = self.base_dir.clone();
49        if command_path.len() > 1 {
50            // Add parent directories
51            for segment in &command_path[..command_path.len() - 1] {
52                path.push(segment);
53            }
54        }
55        // Add the file with extension
56        if let Some(last) = command_path.last() {
57            path.push(format!("{}.{}", last, self.extension));
58        }
59        path
60    }
61
62    /// Get the module file path for a parent command.
63    ///
64    /// # Example
65    ///
66    /// ```ignore
67    /// let paths = HandlerPaths::new("src/handlers", "rs");
68    /// assert_eq!(
69    ///     paths.mod_path(&["db"]),
70    ///     PathBuf::from("src/handlers/db/mod.rs")
71    /// );
72    /// ```
73    pub fn mod_path(&self, command_path: &[&str]) -> PathBuf {
74        let mut path = self.base_dir.clone();
75        for segment in command_path {
76            path.push(segment);
77        }
78        path.push(format!("mod.{}", self.extension));
79        path
80    }
81
82    /// Check if a handler file exists.
83    pub fn exists(&self, command_path: &[&str]) -> bool {
84        self.handler_path(command_path).exists()
85    }
86
87    /// Find orphaned handler files that are no longer in the schema.
88    ///
89    /// Returns paths relative to the base directory.
90    pub fn find_orphans(&self, expected_paths: &HashSet<String>) -> Result<Vec<String>> {
91        let mut orphans = Vec::new();
92        self.scan_for_orphans(&self.base_dir, "", expected_paths, &mut orphans)?;
93        Ok(orphans)
94    }
95
96    fn scan_for_orphans(
97        &self,
98        dir: &Path,
99        prefix: &str,
100        expected: &HashSet<String>,
101        orphans: &mut Vec<String>,
102    ) -> Result<()> {
103        if !dir.exists() {
104            return Ok(());
105        }
106
107        for entry in std::fs::read_dir(dir)? {
108            let entry = entry?;
109            let path = entry.path();
110            let file_name = path.file_name().unwrap().to_string_lossy();
111
112            // Skip mod files
113            if file_name == format!("mod.{}", self.extension) {
114                continue;
115            }
116
117            if path.is_dir() {
118                let new_prefix = if prefix.is_empty() {
119                    file_name.to_string()
120                } else {
121                    format!("{}/{}", prefix, file_name)
122                };
123
124                // Check if this directory is expected
125                if !expected.contains(&new_prefix) {
126                    orphans.push(new_prefix.clone());
127                } else {
128                    self.scan_for_orphans(&path, &new_prefix, expected, orphans)?;
129                }
130            } else if path
131                .extension()
132                .is_some_and(|ext| ext == self.extension.as_str())
133            {
134                let stem = path.file_stem().unwrap().to_string_lossy();
135                let handler_path = if prefix.is_empty() {
136                    stem.to_string()
137                } else {
138                    format!("{}/{}", prefix, stem)
139                };
140
141                if !expected.contains(&handler_path) {
142                    orphans.push(handler_path);
143                }
144            }
145        }
146
147        Ok(())
148    }
149}
150
151/// Collect all handler paths from a schema.
152///
153/// Returns a set of path strings like "db/migrate", "hello".
154pub fn collect_handler_paths(schema: &Schema) -> HashSet<String> {
155    let commands = flatten_commands(schema);
156    let mut paths = HashSet::new();
157
158    for cmd in commands {
159        let path_str = cmd.path_str("/");
160        paths.insert(path_str);
161    }
162
163    paths
164}
165
166/// Collect only leaf handler paths (actual handler files, not parent directories).
167pub fn collect_leaf_handler_paths(schema: &Schema) -> HashSet<String> {
168    let commands = flatten_commands(schema);
169    let mut paths = HashSet::new();
170
171    for cmd in commands {
172        if cmd.is_leaf {
173            paths.insert(cmd.path_str("/"));
174        }
175    }
176
177    paths
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_handler_path_simple() {
186        let paths = HandlerPaths::new("src/handlers", "rs");
187        assert_eq!(
188            paths.handler_path(&["hello"]),
189            PathBuf::from("src/handlers/hello.rs")
190        );
191    }
192
193    #[test]
194    fn test_handler_path_nested() {
195        let paths = HandlerPaths::new("src/handlers", "rs");
196        assert_eq!(
197            paths.handler_path(&["db", "migrate"]),
198            PathBuf::from("src/handlers/db/migrate.rs")
199        );
200    }
201
202    #[test]
203    fn test_handler_path_deeply_nested() {
204        let paths = HandlerPaths::new("src/handlers", "rs");
205        assert_eq!(
206            paths.handler_path(&["config", "user", "set"]),
207            PathBuf::from("src/handlers/config/user/set.rs")
208        );
209    }
210
211    #[test]
212    fn test_mod_path() {
213        let paths = HandlerPaths::new("src/handlers", "rs");
214        assert_eq!(
215            paths.mod_path(&["db"]),
216            PathBuf::from("src/handlers/db/mod.rs")
217        );
218    }
219
220    #[test]
221    fn test_typescript_extension() {
222        let paths = HandlerPaths::new("src/handlers", "ts");
223        assert_eq!(
224            paths.handler_path(&["hello"]),
225            PathBuf::from("src/handlers/hello.ts")
226        );
227    }
228}