baobao_codegen/
handlers.rs

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