baobao_codegen/generation/
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 stub_marker: String,
25}
26
27impl HandlerPaths {
28 pub fn new(
43 base_dir: impl Into<PathBuf>,
44 extension: impl Into<String>,
45 stub_marker: impl Into<String>,
46 ) -> Self {
47 Self {
48 base_dir: base_dir.into(),
49 extension: extension.into(),
50 stub_marker: stub_marker.into(),
51 }
52 }
53
54 pub fn handler_path(&self, command_path: &[&str]) -> PathBuf {
66 let mut path = self.base_dir.clone();
67 if command_path.len() > 1 {
68 for segment in &command_path[..command_path.len() - 1] {
70 path.push(segment);
71 }
72 }
73 if let Some(last) = command_path.last() {
75 path.push(format!("{}.{}", last, self.extension));
76 }
77 path
78 }
79
80 pub fn mod_path(&self, command_path: &[&str]) -> PathBuf {
92 let mut path = self.base_dir.clone();
93 for segment in command_path {
94 path.push(segment);
95 }
96 path.push(format!("mod.{}", self.extension));
97 path
98 }
99
100 pub fn exists(&self, command_path: &[&str]) -> bool {
102 self.handler_path(command_path).exists()
103 }
104
105 pub fn find_orphans(&self, expected_paths: &HashSet<String>) -> Result<Vec<String>> {
109 let mut orphans = Vec::new();
110 self.scan_for_orphans(&self.base_dir, "", expected_paths, &mut orphans)?;
111 Ok(orphans)
112 }
113
114 fn scan_for_orphans(
115 &self,
116 dir: &Path,
117 prefix: &str,
118 expected: &HashSet<String>,
119 orphans: &mut Vec<String>,
120 ) -> Result<()> {
121 if !dir.exists() {
122 return Ok(());
123 }
124
125 for entry in std::fs::read_dir(dir)? {
126 let entry = entry?;
127 let path = entry.path();
128 let file_name = path.file_name().unwrap().to_string_lossy();
129
130 if file_name == format!("mod.{}", self.extension) {
132 continue;
133 }
134
135 if path.is_dir() {
136 let new_prefix = if prefix.is_empty() {
137 file_name.to_string()
138 } else {
139 format!("{}/{}", prefix, file_name)
140 };
141
142 if !expected.contains(&new_prefix) {
144 orphans.push(new_prefix.clone());
145 } else {
146 self.scan_for_orphans(&path, &new_prefix, expected, orphans)?;
147 }
148 } else if path
149 .extension()
150 .is_some_and(|ext| ext == self.extension.as_str())
151 {
152 let stem = path.file_stem().unwrap().to_string_lossy();
153 let handler_path = if prefix.is_empty() {
154 stem.to_string()
155 } else {
156 format!("{}/{}", prefix, stem)
157 };
158
159 if !expected.contains(&handler_path) {
160 orphans.push(handler_path);
161 }
162 }
163 }
164
165 Ok(())
166 }
167
168 pub fn find_orphans_with_status(
173 &self,
174 expected_paths: &HashSet<String>,
175 ) -> Result<Vec<OrphanHandler>> {
176 let mut orphans = Vec::new();
177 self.scan_for_orphans_with_status(&self.base_dir, "", expected_paths, &mut orphans)?;
178 Ok(orphans)
179 }
180
181 fn scan_for_orphans_with_status(
182 &self,
183 dir: &Path,
184 prefix: &str,
185 expected: &HashSet<String>,
186 orphans: &mut Vec<OrphanHandler>,
187 ) -> Result<()> {
188 if !dir.exists() {
189 return Ok(());
190 }
191
192 for entry in std::fs::read_dir(dir)? {
193 let entry = entry?;
194 let path = entry.path();
195 let file_name = path.file_name().unwrap().to_string_lossy();
196
197 if file_name == format!("mod.{}", self.extension) {
199 continue;
200 }
201
202 if path.is_dir() {
203 let new_prefix = if prefix.is_empty() {
204 file_name.to_string()
205 } else {
206 format!("{}/{}", prefix, file_name)
207 };
208
209 if !expected.contains(&new_prefix) {
211 self.collect_all_files(&path, &new_prefix, orphans)?;
213 } else {
214 self.scan_for_orphans_with_status(&path, &new_prefix, expected, orphans)?;
215 }
216 } else if path
217 .extension()
218 .is_some_and(|ext| ext == self.extension.as_str())
219 {
220 let stem = path.file_stem().unwrap().to_string_lossy();
221 let relative_path = if prefix.is_empty() {
222 stem.to_string()
223 } else {
224 format!("{}/{}", prefix, stem)
225 };
226
227 if !expected.contains(&relative_path) {
228 let is_unmodified = self.is_handler_unmodified(&path);
229 orphans.push(OrphanHandler {
230 relative_path,
231 full_path: path,
232 is_unmodified,
233 });
234 }
235 }
236 }
237
238 Ok(())
239 }
240
241 fn collect_all_files(
243 &self,
244 dir: &Path,
245 prefix: &str,
246 orphans: &mut Vec<OrphanHandler>,
247 ) -> Result<()> {
248 if !dir.exists() {
249 return Ok(());
250 }
251
252 for entry in std::fs::read_dir(dir)? {
253 let entry = entry?;
254 let path = entry.path();
255 let file_name = path.file_name().unwrap().to_string_lossy();
256
257 if path.is_dir() {
258 let new_prefix = format!("{}/{}", prefix, file_name);
259 self.collect_all_files(&path, &new_prefix, orphans)?;
260 } else if path
261 .extension()
262 .is_some_and(|ext| ext == self.extension.as_str())
263 {
264 let stem = path.file_stem().unwrap().to_string_lossy();
265 let relative_path = format!("{}/{}", prefix, stem);
266 let is_unmodified = self.is_handler_unmodified(&path);
267 orphans.push(OrphanHandler {
268 relative_path,
269 full_path: path,
270 is_unmodified,
271 });
272 }
273 }
274
275 Ok(())
276 }
277
278 fn is_handler_unmodified(&self, path: &Path) -> bool {
280 std::fs::read_to_string(path)
281 .map(|content| content.contains(&self.stub_marker))
282 .unwrap_or(false)
283 }
284}
285
286#[derive(Debug, Clone)]
288pub struct OrphanHandler {
289 pub relative_path: String,
291 pub full_path: PathBuf,
293 pub is_unmodified: bool,
295}
296
297pub fn find_orphan_commands(
302 commands_dir: &Path,
303 extension: &str,
304 expected_commands: &HashSet<String>,
305) -> Result<Vec<PathBuf>> {
306 let mut orphans = Vec::new();
307
308 if !commands_dir.exists() {
309 return Ok(orphans);
310 }
311
312 for entry in std::fs::read_dir(commands_dir)? {
313 let entry = entry?;
314 let path = entry.path();
315
316 if path.is_dir() {
318 continue;
319 }
320
321 let file_name = path.file_name().unwrap().to_string_lossy();
322 if file_name == format!("mod.{}", extension) {
323 continue;
324 }
325
326 if path.extension().is_none_or(|ext| ext != extension) {
328 continue;
329 }
330
331 let stem = path.file_stem().unwrap().to_string_lossy();
333
334 if !expected_commands.contains(stem.as_ref()) {
336 orphans.push(path);
337 }
338 }
339
340 Ok(orphans)
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 const RUST_STUB_MARKER: &str = "todo!(\"implement";
348 const TS_STUB_MARKER: &str = "// TODO: implement";
349
350 #[test]
351 fn test_handler_path_simple() {
352 let paths = HandlerPaths::new("src/handlers", "rs", RUST_STUB_MARKER);
353 assert_eq!(
354 paths.handler_path(&["hello"]),
355 PathBuf::from("src/handlers/hello.rs")
356 );
357 }
358
359 #[test]
360 fn test_handler_path_nested() {
361 let paths = HandlerPaths::new("src/handlers", "rs", RUST_STUB_MARKER);
362 assert_eq!(
363 paths.handler_path(&["db", "migrate"]),
364 PathBuf::from("src/handlers/db/migrate.rs")
365 );
366 }
367
368 #[test]
369 fn test_handler_path_deeply_nested() {
370 let paths = HandlerPaths::new("src/handlers", "rs", RUST_STUB_MARKER);
371 assert_eq!(
372 paths.handler_path(&["config", "user", "set"]),
373 PathBuf::from("src/handlers/config/user/set.rs")
374 );
375 }
376
377 #[test]
378 fn test_mod_path() {
379 let paths = HandlerPaths::new("src/handlers", "rs", RUST_STUB_MARKER);
380 assert_eq!(
381 paths.mod_path(&["db"]),
382 PathBuf::from("src/handlers/db/mod.rs")
383 );
384 }
385
386 #[test]
387 fn test_typescript_extension() {
388 let paths = HandlerPaths::new("src/handlers", "ts", TS_STUB_MARKER);
389 assert_eq!(
390 paths.handler_path(&["hello"]),
391 PathBuf::from("src/handlers/hello.ts")
392 );
393 }
394}