agentic_navigation_guide/
errors.rs1use std::path::PathBuf;
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, AppError>;
8
9#[derive(Debug, Error)]
11pub enum AppError {
12 #[error("syntax error: {0}")]
14 Syntax(#[from] SyntaxError),
15
16 #[error("semantic error: {0}")]
18 Semantic(#[from] SemanticError),
19
20 #[error("I/O error: {0}")]
22 Io(#[from] std::io::Error),
23
24 #[error("invalid glob pattern: {0}")]
26 GlobPattern(#[from] globset::Error),
27
28 #[error("filesystem walk error: {0}")]
30 WalkDir(#[from] walkdir::Error),
31
32 #[error("{0}")]
34 Other(String),
35}
36
37#[derive(Debug, Error, PartialEq, Eq)]
39pub enum SyntaxError {
40 #[error("line {line}: missing opening <agentic-navigation-guide> marker")]
42 MissingOpeningMarker { line: usize },
43
44 #[error("line {line}: missing closing </agentic-navigation-guide> marker")]
46 MissingClosingMarker { line: usize },
47
48 #[error("line {line}: multiple <agentic-navigation-guide> blocks found")]
50 MultipleGuideBlocks { line: usize },
51
52 #[error("empty navigation guide block")]
54 EmptyGuideBlock,
55
56 #[error("line {line}: invalid list format - must start with '-'")]
58 InvalidListFormat { line: usize },
59
60 #[error("line {line}: directory '{path}' must end with '/'")]
62 DirectoryMissingSlash { line: usize, path: String },
63
64 #[error("line {line}: invalid special directory '{path}' (. and .. are not allowed)")]
66 InvalidSpecialDirectory { line: usize, path: String },
67
68 #[error("line {line}: inconsistent indentation - expected {expected} spaces, found {found}")]
70 InconsistentIndentation {
71 line: usize,
72 expected: usize,
73 found: usize,
74 },
75
76 #[error("line {line}: invalid indentation level - must be a multiple of the indent size")]
78 InvalidIndentationLevel { line: usize },
79
80 #[error("line {line}: blank lines are not allowed within the guide block")]
82 BlankLineInGuide { line: usize },
83
84 #[error("line {line}: invalid path format '{path}'")]
86 InvalidPathFormat { line: usize, path: String },
87
88 #[error("line {line}: invalid wildcard choice syntax in '{path}': {message}")]
90 InvalidWildcardSyntax {
91 line: usize,
92 path: String,
93 message: String,
94 },
95
96 #[error("line {line}: invalid comment format - comments must be separated by '#'")]
98 InvalidCommentFormat { line: usize },
99
100 #[error("line {line}: placeholder entries (...) cannot be adjacent to each other")]
102 AdjacentPlaceholders { line: usize },
103
104 #[error("line {line}: placeholder entries (...) cannot have child elements")]
106 PlaceholderWithChildren { line: usize },
107}
108
109#[derive(Debug, Error, PartialEq, Eq)]
111pub enum SemanticError {
112 #[error("line {line}: {item_type} '{path}' not found at {full_path}")]
114 ItemNotFound {
115 line: usize,
116 item_type: String,
117 path: String,
118 full_path: PathBuf,
119 },
120
121 #[error("line {line}: expected {expected} but found {found} at '{path}'")]
123 TypeMismatch {
124 line: usize,
125 expected: String,
126 found: String,
127 path: String,
128 },
129
130 #[error("line {line}: '{child}' is not a child of '{parent}'")]
132 InvalidNesting {
133 line: usize,
134 child: String,
135 parent: String,
136 },
137
138 #[error("line {line}: symlink '{path}' points to '{actual}' but guide specifies '{expected}'")]
140 SymlinkTargetMismatch {
141 line: usize,
142 path: String,
143 expected: String,
144 actual: String,
145 },
146
147 #[error("line {line}: permission denied accessing '{path}'")]
149 PermissionDenied { line: usize, path: String },
150
151 #[error(
153 "line {line}: placeholder (...) must refer to at least one unlisted item in '{parent}'"
154 )]
155 PlaceholderNoUnmentionedItems { line: usize, parent: String },
156}
157
158impl SyntaxError {
159 pub fn line_number(&self) -> Option<usize> {
161 match self {
162 Self::MissingOpeningMarker { line }
163 | Self::MissingClosingMarker { line }
164 | Self::MultipleGuideBlocks { line }
165 | Self::InvalidListFormat { line }
166 | Self::DirectoryMissingSlash { line, .. }
167 | Self::InvalidSpecialDirectory { line, .. }
168 | Self::InconsistentIndentation { line, .. }
169 | Self::InvalidIndentationLevel { line }
170 | Self::BlankLineInGuide { line }
171 | Self::InvalidPathFormat { line, .. }
172 | Self::InvalidWildcardSyntax { line, .. }
173 | Self::InvalidCommentFormat { line }
174 | Self::AdjacentPlaceholders { line }
175 | Self::PlaceholderWithChildren { line } => Some(*line),
176 Self::EmptyGuideBlock => None,
177 }
178 }
179}
180
181impl SemanticError {
182 pub fn line_number(&self) -> usize {
184 match self {
185 Self::ItemNotFound { line, .. }
186 | Self::TypeMismatch { line, .. }
187 | Self::InvalidNesting { line, .. }
188 | Self::SymlinkTargetMismatch { line, .. }
189 | Self::PermissionDenied { line, .. }
190 | Self::PlaceholderNoUnmentionedItems { line, .. } => *line,
191 }
192 }
193}
194
195pub struct ErrorFormatter;
197
198impl ErrorFormatter {
199 pub fn format_with_context(error: &AppError, file_content: Option<&str>) -> String {
201 match error {
202 AppError::Syntax(e) => {
203 if let Some(line_num) = e.line_number() {
204 Self::format_with_line_context(e.to_string(), line_num, file_content)
205 } else {
206 e.to_string()
207 }
208 }
209 AppError::Semantic(e) => {
210 Self::format_with_line_context(e.to_string(), e.line_number(), file_content)
211 }
212 _ => error.to_string(),
213 }
214 }
215
216 fn format_with_line_context(
217 error_msg: String,
218 line_num: usize,
219 file_content: Option<&str>,
220 ) -> String {
221 if let Some(content) = file_content {
222 if let Some(line) = content.lines().nth(line_num.saturating_sub(1)) {
223 format!("{}\n {}", error_msg, line.trim())
224 } else {
225 error_msg
226 }
227 } else {
228 error_msg
229 }
230 }
231}