baobao_codegen/
handlers.rs1use std::{
4 collections::HashSet,
5 path::{Path, PathBuf},
6};
7
8use eyre::Result;
9
10#[derive(Debug, Clone)]
17pub struct HandlerPaths {
18 base_dir: PathBuf,
20 extension: String,
22}
23
24impl HandlerPaths {
25 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 pub fn handler_path(&self, command_path: &[&str]) -> PathBuf {
45 let mut path = self.base_dir.clone();
46 if command_path.len() > 1 {
47 for segment in &command_path[..command_path.len() - 1] {
49 path.push(segment);
50 }
51 }
52 if let Some(last) = command_path.last() {
54 path.push(format!("{}.{}", last, self.extension));
55 }
56 path
57 }
58
59 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 pub fn exists(&self, command_path: &[&str]) -> bool {
81 self.handler_path(command_path).exists()
82 }
83
84 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 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 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}