agentic_navigation_guide/
verifier.rs1use 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 for item in &guide.items {
28 self.verify_item(item, &self.root_path)?;
29 }
30
31 Ok(())
32 }
33
34 fn verify_item(&self, item: &NavigationGuideLine, parent_path: &Path) -> Result<()> {
36 let item_path = parent_path.join(item.path());
37
38 if !item_path.exists() {
40 return Err(SemanticError::ItemNotFound {
41 line: item.line_number,
42 item_type: self.get_item_type_string(item),
43 path: item.path().to_string(),
44 full_path: item_path,
45 }
46 .into());
47 }
48
49 match &item.item {
51 FilesystemItem::Directory { children, .. } => {
52 if !item_path.is_dir() {
53 return Err(SemanticError::TypeMismatch {
54 line: item.line_number,
55 expected: "directory".to_string(),
56 found: if item_path.is_file() {
57 "file".to_string()
58 } else {
59 "symlink".to_string()
60 },
61 path: item.path().to_string(),
62 }
63 .into());
64 }
65
66 for child in children {
68 self.verify_item(child, &item_path)?;
69 }
70 }
71 FilesystemItem::File { .. } => {
72 if !item_path.is_file() {
73 return Err(SemanticError::TypeMismatch {
74 line: item.line_number,
75 expected: "file".to_string(),
76 found: if item_path.is_dir() {
77 "directory".to_string()
78 } else {
79 "symlink".to_string()
80 },
81 path: item.path().to_string(),
82 }
83 .into());
84 }
85 }
86 FilesystemItem::Symlink { target, .. } => {
87 let metadata = match std::fs::symlink_metadata(&item_path) {
88 Ok(m) => m,
89 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
90 return Err(SemanticError::PermissionDenied {
91 line: item.line_number,
92 path: item.path().to_string(),
93 }
94 .into());
95 }
96 Err(e) => return Err(e.into()),
97 };
98
99 if !metadata.is_symlink() {
100 return Err(SemanticError::TypeMismatch {
101 line: item.line_number,
102 expected: "symlink".to_string(),
103 found: if item_path.is_dir() {
104 "directory".to_string()
105 } else {
106 "file".to_string()
107 },
108 path: item.path().to_string(),
109 }
110 .into());
111 }
112
113 if let Some(expected_target) = target {
115 if let Ok(actual_target) = std::fs::read_link(&item_path) {
116 if actual_target.to_string_lossy() != *expected_target {
117 return Err(SemanticError::SymlinkTargetMismatch {
118 line: item.line_number,
119 path: item.path().to_string(),
120 expected: expected_target.clone(),
121 actual: actual_target.to_string_lossy().to_string(),
122 }
123 .into());
124 }
125 }
126 }
127 }
128 }
129
130 Ok(())
131 }
132
133 fn get_item_type_string(&self, item: &NavigationGuideLine) -> String {
135 match &item.item {
136 FilesystemItem::Directory { .. } => "directory".to_string(),
137 FilesystemItem::File { .. } => "file".to_string(),
138 FilesystemItem::Symlink { .. } => "symlink".to_string(),
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use tempfile::TempDir;
147
148 #[test]
149 fn test_verify_missing_file() {
150 let temp_dir = TempDir::new().unwrap();
151 let verifier = Verifier::new(temp_dir.path());
152
153 let guide = NavigationGuide {
154 items: vec![NavigationGuideLine {
155 line_number: 1,
156 indent_level: 0,
157 item: FilesystemItem::File {
158 path: "missing.txt".to_string(),
159 comment: None,
160 },
161 }],
162 prologue: None,
163 epilogue: None,
164 };
165
166 let result = verifier.verify(&guide);
167 assert!(matches!(
168 result,
169 Err(crate::errors::AppError::Semantic(
170 SemanticError::ItemNotFound { .. }
171 ))
172 ));
173 }
174}