1use std::{
4 collections::HashSet,
5 path::{Path, PathBuf},
6};
7
8use eyre::Result;
9
10const HANDLER_STUB_MARKER: &str = "todo!(\"implement";
12
13#[derive(Debug, Clone)]
20pub struct HandlerPaths {
21 base_dir: PathBuf,
23 extension: String,
25}
26
27impl HandlerPaths {
28 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 pub fn handler_path(&self, command_path: &[&str]) -> PathBuf {
48 let mut path = self.base_dir.clone();
49 if command_path.len() > 1 {
50 for segment in &command_path[..command_path.len() - 1] {
52 path.push(segment);
53 }
54 }
55 if let Some(last) = command_path.last() {
57 path.push(format!("{}.{}", last, self.extension));
58 }
59 path
60 }
61
62 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 pub fn exists(&self, command_path: &[&str]) -> bool {
84 self.handler_path(command_path).exists()
85 }
86
87 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 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 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 pub fn find_orphans_with_status(
155 &self,
156 expected_paths: &HashSet<String>,
157 ) -> Result<Vec<OrphanHandler>> {
158 let mut orphans = Vec::new();
159 self.scan_for_orphans_with_status(&self.base_dir, "", expected_paths, &mut orphans)?;
160 Ok(orphans)
161 }
162
163 fn scan_for_orphans_with_status(
164 &self,
165 dir: &Path,
166 prefix: &str,
167 expected: &HashSet<String>,
168 orphans: &mut Vec<OrphanHandler>,
169 ) -> Result<()> {
170 if !dir.exists() {
171 return Ok(());
172 }
173
174 for entry in std::fs::read_dir(dir)? {
175 let entry = entry?;
176 let path = entry.path();
177 let file_name = path.file_name().unwrap().to_string_lossy();
178
179 if file_name == format!("mod.{}", self.extension) {
181 continue;
182 }
183
184 if path.is_dir() {
185 let new_prefix = if prefix.is_empty() {
186 file_name.to_string()
187 } else {
188 format!("{}/{}", prefix, file_name)
189 };
190
191 if !expected.contains(&new_prefix) {
193 self.collect_all_files(&path, &new_prefix, orphans)?;
195 } else {
196 self.scan_for_orphans_with_status(&path, &new_prefix, expected, orphans)?;
197 }
198 } else if path
199 .extension()
200 .is_some_and(|ext| ext == self.extension.as_str())
201 {
202 let stem = path.file_stem().unwrap().to_string_lossy();
203 let relative_path = if prefix.is_empty() {
204 stem.to_string()
205 } else {
206 format!("{}/{}", prefix, stem)
207 };
208
209 if !expected.contains(&relative_path) {
210 let is_unmodified = Self::is_handler_unmodified(&path);
211 orphans.push(OrphanHandler {
212 relative_path,
213 full_path: path,
214 is_unmodified,
215 });
216 }
217 }
218 }
219
220 Ok(())
221 }
222
223 fn collect_all_files(
225 &self,
226 dir: &Path,
227 prefix: &str,
228 orphans: &mut Vec<OrphanHandler>,
229 ) -> Result<()> {
230 if !dir.exists() {
231 return Ok(());
232 }
233
234 for entry in std::fs::read_dir(dir)? {
235 let entry = entry?;
236 let path = entry.path();
237 let file_name = path.file_name().unwrap().to_string_lossy();
238
239 if path.is_dir() {
240 let new_prefix = format!("{}/{}", prefix, file_name);
241 self.collect_all_files(&path, &new_prefix, orphans)?;
242 } else if path
243 .extension()
244 .is_some_and(|ext| ext == self.extension.as_str())
245 {
246 let stem = path.file_stem().unwrap().to_string_lossy();
247 let relative_path = format!("{}/{}", prefix, stem);
248 let is_unmodified = Self::is_handler_unmodified(&path);
249 orphans.push(OrphanHandler {
250 relative_path,
251 full_path: path,
252 is_unmodified,
253 });
254 }
255 }
256
257 Ok(())
258 }
259
260 fn is_handler_unmodified(path: &Path) -> bool {
262 std::fs::read_to_string(path)
263 .map(|content| content.contains(HANDLER_STUB_MARKER))
264 .unwrap_or(false)
265 }
266}
267
268#[derive(Debug, Clone)]
270pub struct OrphanHandler {
271 pub relative_path: String,
273 pub full_path: PathBuf,
275 pub is_unmodified: bool,
277}
278
279pub fn find_orphan_commands(
284 commands_dir: &Path,
285 extension: &str,
286 expected_commands: &HashSet<String>,
287) -> Result<Vec<PathBuf>> {
288 let mut orphans = Vec::new();
289
290 if !commands_dir.exists() {
291 return Ok(orphans);
292 }
293
294 for entry in std::fs::read_dir(commands_dir)? {
295 let entry = entry?;
296 let path = entry.path();
297
298 if path.is_dir() {
300 continue;
301 }
302
303 let file_name = path.file_name().unwrap().to_string_lossy();
304 if file_name == format!("mod.{}", extension) {
305 continue;
306 }
307
308 if !path.extension().is_some_and(|ext| ext == extension) {
310 continue;
311 }
312
313 let stem = path.file_stem().unwrap().to_string_lossy();
315
316 if !expected_commands.contains(stem.as_ref()) {
318 orphans.push(path);
319 }
320 }
321
322 Ok(orphans)
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_handler_path_simple() {
331 let paths = HandlerPaths::new("src/handlers", "rs");
332 assert_eq!(
333 paths.handler_path(&["hello"]),
334 PathBuf::from("src/handlers/hello.rs")
335 );
336 }
337
338 #[test]
339 fn test_handler_path_nested() {
340 let paths = HandlerPaths::new("src/handlers", "rs");
341 assert_eq!(
342 paths.handler_path(&["db", "migrate"]),
343 PathBuf::from("src/handlers/db/migrate.rs")
344 );
345 }
346
347 #[test]
348 fn test_handler_path_deeply_nested() {
349 let paths = HandlerPaths::new("src/handlers", "rs");
350 assert_eq!(
351 paths.handler_path(&["config", "user", "set"]),
352 PathBuf::from("src/handlers/config/user/set.rs")
353 );
354 }
355
356 #[test]
357 fn test_mod_path() {
358 let paths = HandlerPaths::new("src/handlers", "rs");
359 assert_eq!(
360 paths.mod_path(&["db"]),
361 PathBuf::from("src/handlers/db/mod.rs")
362 );
363 }
364
365 #[test]
366 fn test_typescript_extension() {
367 let paths = HandlerPaths::new("src/handlers", "ts");
368 assert_eq!(
369 paths.handler_path(&["hello"]),
370 PathBuf::from("src/handlers/hello.ts")
371 );
372 }
373}