1use crate::errors::{Result, SemanticError};
4use crate::types::{FilesystemItem, NavigationGuide, NavigationGuideLine};
5use std::path::{Path, PathBuf};
6
7pub struct Verifier {
9 root_path: PathBuf,
11}
12
13impl Verifier {
14 pub fn new(root_path: &Path) -> Self {
16 Self {
17 root_path: root_path.to_path_buf(),
18 }
19 }
20
21 pub fn verify(&self, guide: &NavigationGuide) -> Result<()> {
23 crate::validator::Validator::new().validate_syntax(guide)?;
25
26 let mut mentioned_names = std::collections::HashSet::new();
28 for item in &guide.items {
29 if !item.is_placeholder() {
30 mentioned_names.insert(item.path().to_string());
31 }
32 }
33
34 for item in &guide.items {
36 if item.is_placeholder() {
37 self.verify_placeholder_with_context(item, &self.root_path, &mentioned_names)?;
38 } else {
39 self.verify_item(item, &self.root_path)?;
40 }
41 }
42
43 Ok(())
44 }
45
46 fn verify_item(&self, item: &NavigationGuideLine, parent_path: &Path) -> Result<()> {
48 if item.is_placeholder() {
50 return self.verify_placeholder(item, parent_path);
51 }
52
53 let item_path = parent_path.join(item.path());
54
55 if !item_path.exists() {
57 return Err(SemanticError::ItemNotFound {
58 line: item.line_number,
59 item_type: self.get_item_type_string(item),
60 path: item.path().to_string(),
61 full_path: item_path,
62 }
63 .into());
64 }
65
66 match &item.item {
68 FilesystemItem::Directory { children, .. } => {
69 if !item_path.is_dir() {
70 return Err(SemanticError::TypeMismatch {
71 line: item.line_number,
72 expected: "directory".to_string(),
73 found: if item_path.is_file() {
74 "file".to_string()
75 } else {
76 "symlink".to_string()
77 },
78 path: item.path().to_string(),
79 }
80 .into());
81 }
82
83 let mut mentioned_names = std::collections::HashSet::new();
85 for child in children {
86 if !child.is_placeholder() {
87 mentioned_names.insert(child.path().to_string());
88 }
89 }
90
91 for child in children {
92 if child.is_placeholder() {
93 self.verify_placeholder_with_context(child, &item_path, &mentioned_names)?;
94 } else {
95 self.verify_item(child, &item_path)?;
96 }
97 }
98 }
99 FilesystemItem::File { .. } => {
100 if !item_path.is_file() {
101 return Err(SemanticError::TypeMismatch {
102 line: item.line_number,
103 expected: "file".to_string(),
104 found: if item_path.is_dir() {
105 "directory".to_string()
106 } else {
107 "symlink".to_string()
108 },
109 path: item.path().to_string(),
110 }
111 .into());
112 }
113 }
114 FilesystemItem::Symlink { target, .. } => {
115 let metadata = match std::fs::symlink_metadata(&item_path) {
116 Ok(m) => m,
117 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
118 return Err(SemanticError::PermissionDenied {
119 line: item.line_number,
120 path: item.path().to_string(),
121 }
122 .into());
123 }
124 Err(e) => return Err(e.into()),
125 };
126
127 if !metadata.is_symlink() {
128 return Err(SemanticError::TypeMismatch {
129 line: item.line_number,
130 expected: "symlink".to_string(),
131 found: if item_path.is_dir() {
132 "directory".to_string()
133 } else {
134 "file".to_string()
135 },
136 path: item.path().to_string(),
137 }
138 .into());
139 }
140
141 if let Some(expected_target) = target {
143 if let Ok(actual_target) = std::fs::read_link(&item_path) {
144 if actual_target.to_string_lossy() != *expected_target {
145 return Err(SemanticError::SymlinkTargetMismatch {
146 line: item.line_number,
147 path: item.path().to_string(),
148 expected: expected_target.clone(),
149 actual: actual_target.to_string_lossy().to_string(),
150 }
151 .into());
152 }
153 }
154 }
155 }
156 FilesystemItem::Placeholder { .. } => {
157 unreachable!("Placeholder should have been handled earlier");
160 }
161 }
162
163 Ok(())
164 }
165
166 fn get_item_type_string(&self, item: &NavigationGuideLine) -> String {
168 match &item.item {
169 FilesystemItem::Directory { .. } => "directory".to_string(),
170 FilesystemItem::File { .. } => "file".to_string(),
171 FilesystemItem::Symlink { .. } => "symlink".to_string(),
172 FilesystemItem::Placeholder { .. } => "placeholder".to_string(),
173 }
174 }
175
176 fn verify_placeholder(&self, item: &NavigationGuideLine, parent_path: &Path) -> Result<()> {
178 let mentioned_names = std::collections::HashSet::new();
181 self.verify_placeholder_with_context(item, parent_path, &mentioned_names)
182 }
183
184 fn verify_placeholder_with_context(
186 &self,
187 item: &NavigationGuideLine,
188 parent_path: &Path,
189 mentioned_names: &std::collections::HashSet<String>,
190 ) -> Result<()> {
191 let entries = match std::fs::read_dir(parent_path) {
193 Ok(entries) => entries,
194 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
195 return Err(SemanticError::PermissionDenied {
196 line: item.line_number,
197 path: parent_path.to_string_lossy().to_string(),
198 }
199 .into());
200 }
201 Err(e) => return Err(e.into()),
202 };
203
204 let mut unmentioned_count = 0;
206 for entry in entries.flatten() {
207 if let Some(name) = entry.file_name().to_str() {
208 if !mentioned_names.contains(name) {
209 unmentioned_count += 1;
210 }
211 }
212 }
213
214 if unmentioned_count == 0 {
215 if item.comment().is_none() {
218 return Err(SemanticError::PlaceholderNoUnmentionedItems {
219 line: item.line_number,
220 parent: parent_path.to_string_lossy().to_string(),
221 }
222 .into());
223 }
224 }
226
227 Ok(())
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use tempfile::TempDir;
235
236 #[test]
237 fn test_verify_missing_file() {
238 let temp_dir = TempDir::new().unwrap();
239 let verifier = Verifier::new(temp_dir.path());
240
241 let guide = NavigationGuide {
242 items: vec![NavigationGuideLine {
243 line_number: 1,
244 indent_level: 0,
245 item: FilesystemItem::File {
246 path: "missing.txt".to_string(),
247 comment: None,
248 },
249 }],
250 prologue: None,
251 epilogue: None,
252 ignore: false,
253 };
254
255 let result = verifier.verify(&guide);
256 assert!(matches!(
257 result,
258 Err(crate::errors::AppError::Semantic(
259 SemanticError::ItemNotFound { .. }
260 ))
261 ));
262 }
263
264 #[test]
265 fn test_verify_placeholder_with_unmentioned_items() {
266 let temp_dir = TempDir::new().unwrap();
267
268 std::fs::write(temp_dir.path().join("main.rs"), "").unwrap();
270 std::fs::write(temp_dir.path().join("lib.rs"), "").unwrap();
271 std::fs::write(temp_dir.path().join("mod.rs"), "").unwrap();
272
273 let verifier = Verifier::new(temp_dir.path());
274
275 let guide = NavigationGuide {
276 items: vec![
277 NavigationGuideLine {
278 line_number: 1,
279 indent_level: 0,
280 item: FilesystemItem::File {
281 path: "main.rs".to_string(),
282 comment: None,
283 },
284 },
285 NavigationGuideLine {
286 line_number: 2,
287 indent_level: 0,
288 item: FilesystemItem::Placeholder {
289 comment: Some("other source files".to_string()),
290 },
291 },
292 ],
293 prologue: None,
294 epilogue: None,
295 ignore: false,
296 };
297
298 let result = verifier.verify(&guide);
300 assert!(result.is_ok());
301 }
302
303 #[test]
304 fn test_verify_placeholder_with_comment_no_items() {
305 let temp_dir = TempDir::new().unwrap();
306
307 std::fs::write(temp_dir.path().join("main.rs"), "").unwrap();
309
310 let verifier = Verifier::new(temp_dir.path());
311
312 let guide = NavigationGuide {
313 items: vec![
314 NavigationGuideLine {
315 line_number: 1,
316 indent_level: 0,
317 item: FilesystemItem::File {
318 path: "main.rs".to_string(),
319 comment: None,
320 },
321 },
322 NavigationGuideLine {
323 line_number: 2,
324 indent_level: 0,
325 item: FilesystemItem::Placeholder {
326 comment: Some("future files will appear here".to_string()),
327 },
328 },
329 ],
330 prologue: None,
331 epilogue: None,
332 ignore: false,
333 };
334
335 let result = verifier.verify(&guide);
337 assert!(result.is_ok());
338 }
339
340 #[test]
341 fn test_verify_placeholder_without_comment_no_items() {
342 let temp_dir = TempDir::new().unwrap();
343
344 std::fs::write(temp_dir.path().join("main.rs"), "").unwrap();
346
347 let verifier = Verifier::new(temp_dir.path());
348
349 let guide = NavigationGuide {
350 items: vec![
351 NavigationGuideLine {
352 line_number: 1,
353 indent_level: 0,
354 item: FilesystemItem::File {
355 path: "main.rs".to_string(),
356 comment: None,
357 },
358 },
359 NavigationGuideLine {
360 line_number: 2,
361 indent_level: 0,
362 item: FilesystemItem::Placeholder { comment: None },
363 },
364 ],
365 prologue: None,
366 epilogue: None,
367 ignore: false,
368 };
369
370 let result = verifier.verify(&guide);
372 assert!(matches!(
373 result,
374 Err(crate::errors::AppError::Semantic(
375 SemanticError::PlaceholderNoUnmentionedItems { .. }
376 ))
377 ));
378 }
379
380 #[test]
381 fn test_verify_placeholder_in_directory() {
382 let temp_dir = TempDir::new().unwrap();
383 let src_dir = temp_dir.path().join("src");
384 std::fs::create_dir(&src_dir).unwrap();
385
386 std::fs::write(src_dir.join("main.rs"), "").unwrap();
388 std::fs::write(src_dir.join("lib.rs"), "").unwrap();
389 std::fs::write(src_dir.join("utils.rs"), "").unwrap();
390
391 let verifier = Verifier::new(temp_dir.path());
392
393 let guide = NavigationGuide {
394 items: vec![NavigationGuideLine {
395 line_number: 1,
396 indent_level: 0,
397 item: FilesystemItem::Directory {
398 path: "src".to_string(),
399 comment: None,
400 children: vec![
401 NavigationGuideLine {
402 line_number: 2,
403 indent_level: 1,
404 item: FilesystemItem::File {
405 path: "main.rs".to_string(),
406 comment: None,
407 },
408 },
409 NavigationGuideLine {
410 line_number: 3,
411 indent_level: 1,
412 item: FilesystemItem::Placeholder {
413 comment: Some("other modules".to_string()),
414 },
415 },
416 ],
417 },
418 }],
419 prologue: None,
420 epilogue: None,
421 ignore: false,
422 };
423
424 let result = verifier.verify(&guide);
426 assert!(result.is_ok());
427 }
428
429 #[test]
430 fn test_verify_placeholder_in_empty_directory() {
431 let temp_dir = TempDir::new().unwrap();
432 let src_dir = temp_dir.path().join("src");
433 std::fs::create_dir(&src_dir).unwrap();
434
435 let verifier = Verifier::new(temp_dir.path());
436
437 let guide = NavigationGuide {
438 items: vec![NavigationGuideLine {
439 line_number: 1,
440 indent_level: 0,
441 item: FilesystemItem::Directory {
442 path: "src".to_string(),
443 comment: None,
444 children: vec![NavigationGuideLine {
445 line_number: 2,
446 indent_level: 1,
447 item: FilesystemItem::Placeholder {
448 comment: Some("future files".to_string()),
449 },
450 }],
451 },
452 }],
453 prologue: None,
454 epilogue: None,
455 ignore: false,
456 };
457
458 let result = verifier.verify(&guide);
460 assert!(result.is_ok());
461 }
462
463 #[test]
464 fn test_verify_placeholder_in_empty_directory_no_comment() {
465 let temp_dir = TempDir::new().unwrap();
466 let src_dir = temp_dir.path().join("src");
467 std::fs::create_dir(&src_dir).unwrap();
468
469 let verifier = Verifier::new(temp_dir.path());
470
471 let guide = NavigationGuide {
472 items: vec![NavigationGuideLine {
473 line_number: 1,
474 indent_level: 0,
475 item: FilesystemItem::Directory {
476 path: "src".to_string(),
477 comment: None,
478 children: vec![NavigationGuideLine {
479 line_number: 2,
480 indent_level: 1,
481 item: FilesystemItem::Placeholder { comment: None },
482 }],
483 },
484 }],
485 prologue: None,
486 epilogue: None,
487 ignore: false,
488 };
489
490 let result = verifier.verify(&guide);
492 assert!(matches!(
493 result,
494 Err(crate::errors::AppError::Semantic(
495 SemanticError::PlaceholderNoUnmentionedItems { .. }
496 ))
497 ));
498 }
499
500 #[test]
501 fn test_multiple_placeholders_mixed_comments() {
502 let temp_dir = TempDir::new().unwrap();
503 let src_dir = temp_dir.path().join("src");
504 std::fs::create_dir(&src_dir).unwrap();
505
506 std::fs::write(src_dir.join("main.rs"), "").unwrap();
508 std::fs::write(src_dir.join("lib.rs"), "").unwrap();
509 std::fs::write(src_dir.join("utils.rs"), "").unwrap();
510
511 let verifier = Verifier::new(temp_dir.path());
512
513 let guide = NavigationGuide {
514 items: vec![NavigationGuideLine {
515 line_number: 1,
516 indent_level: 0,
517 item: FilesystemItem::Directory {
518 path: "src".to_string(),
519 comment: None,
520 children: vec![
521 NavigationGuideLine {
522 line_number: 2,
523 indent_level: 1,
524 item: FilesystemItem::File {
525 path: "main.rs".to_string(),
526 comment: None,
527 },
528 },
529 NavigationGuideLine {
530 line_number: 3,
531 indent_level: 1,
532 item: FilesystemItem::Placeholder {
533 comment: Some("other modules".to_string()),
534 },
535 },
536 NavigationGuideLine {
537 line_number: 4,
538 indent_level: 1,
539 item: FilesystemItem::File {
540 path: "lib.rs".to_string(),
541 comment: None,
542 },
543 },
544 NavigationGuideLine {
545 line_number: 5,
546 indent_level: 1,
547 item: FilesystemItem::Placeholder {
548 comment: Some("future expansion files".to_string()),
549 },
550 },
551 ],
552 },
553 }],
554 prologue: None,
555 epilogue: None,
556 ignore: false,
557 };
558
559 let result = verifier.verify(&guide);
561 assert!(result.is_ok());
562 }
563
564 #[test]
565 fn test_placeholder_with_comment_in_nested_directory() {
566 let temp_dir = TempDir::new().unwrap();
567 let nested_dir = temp_dir.path().join("src/modules/auth");
568 std::fs::create_dir_all(&nested_dir).unwrap();
569
570 std::fs::write(nested_dir.join("login.rs"), "").unwrap();
572
573 let verifier = Verifier::new(temp_dir.path());
574
575 let guide = NavigationGuide {
576 items: vec![NavigationGuideLine {
577 line_number: 1,
578 indent_level: 0,
579 item: FilesystemItem::Directory {
580 path: "src".to_string(),
581 comment: None,
582 children: vec![NavigationGuideLine {
583 line_number: 2,
584 indent_level: 1,
585 item: FilesystemItem::Directory {
586 path: "modules".to_string(),
587 comment: None,
588 children: vec![NavigationGuideLine {
589 line_number: 3,
590 indent_level: 2,
591 item: FilesystemItem::Directory {
592 path: "auth".to_string(),
593 comment: None,
594 children: vec![
595 NavigationGuideLine {
596 line_number: 4,
597 indent_level: 3,
598 item: FilesystemItem::File {
599 path: "login.rs".to_string(),
600 comment: None,
601 },
602 },
603 NavigationGuideLine {
604 line_number: 5,
605 indent_level: 3,
606 item: FilesystemItem::Placeholder {
607 comment: Some(
608 "additional auth features coming soon"
609 .to_string(),
610 ),
611 },
612 },
613 ],
614 },
615 }],
616 },
617 }],
618 },
619 }],
620 prologue: None,
621 epilogue: None,
622 ignore: false,
623 };
624
625 let result = verifier.verify(&guide);
627 assert!(result.is_ok());
628 }
629
630 #[test]
631 fn test_placeholder_without_comment_with_unmentioned() {
632 let temp_dir = TempDir::new().unwrap();
633
634 std::fs::write(temp_dir.path().join("main.rs"), "").unwrap();
636 std::fs::write(temp_dir.path().join("lib.rs"), "").unwrap();
637 std::fs::write(temp_dir.path().join("utils.rs"), "").unwrap();
638
639 let verifier = Verifier::new(temp_dir.path());
640
641 let guide = NavigationGuide {
642 items: vec![
643 NavigationGuideLine {
644 line_number: 1,
645 indent_level: 0,
646 item: FilesystemItem::File {
647 path: "main.rs".to_string(),
648 comment: None,
649 },
650 },
651 NavigationGuideLine {
652 line_number: 2,
653 indent_level: 0,
654 item: FilesystemItem::Placeholder { comment: None },
655 },
656 ],
657 prologue: None,
658 epilogue: None,
659 ignore: false,
660 };
661
662 let result = verifier.verify(&guide);
664 assert!(result.is_ok());
665 }
666}