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 comment format - comments must be separated by '#'")]
90 InvalidCommentFormat { line: usize },
91
92 #[error("line {line}: placeholder entries (...) cannot be adjacent to each other")]
94 AdjacentPlaceholders { line: usize },
95
96 #[error("line {line}: placeholder entries (...) cannot have child elements")]
98 PlaceholderWithChildren { line: usize },
99}
100
101#[derive(Debug, Error, PartialEq, Eq)]
103pub enum SemanticError {
104 #[error("line {line}: {item_type} '{path}' not found at {full_path}")]
106 ItemNotFound {
107 line: usize,
108 item_type: String,
109 path: String,
110 full_path: PathBuf,
111 },
112
113 #[error("line {line}: expected {expected} but found {found} at '{path}'")]
115 TypeMismatch {
116 line: usize,
117 expected: String,
118 found: String,
119 path: String,
120 },
121
122 #[error("line {line}: '{child}' is not a child of '{parent}'")]
124 InvalidNesting {
125 line: usize,
126 child: String,
127 parent: String,
128 },
129
130 #[error("line {line}: symlink '{path}' points to '{actual}' but guide specifies '{expected}'")]
132 SymlinkTargetMismatch {
133 line: usize,
134 path: String,
135 expected: String,
136 actual: String,
137 },
138
139 #[error("line {line}: permission denied accessing '{path}'")]
141 PermissionDenied { line: usize, path: String },
142
143 #[error(
145 "line {line}: placeholder (...) must refer to at least one unlisted item in '{parent}'"
146 )]
147 PlaceholderNoUnmentionedItems { line: usize, parent: String },
148}
149
150impl SyntaxError {
151 pub fn line_number(&self) -> Option<usize> {
153 match self {
154 Self::MissingOpeningMarker { line }
155 | Self::MissingClosingMarker { line }
156 | Self::MultipleGuideBlocks { line }
157 | Self::InvalidListFormat { line }
158 | Self::DirectoryMissingSlash { line, .. }
159 | Self::InvalidSpecialDirectory { line, .. }
160 | Self::InconsistentIndentation { line, .. }
161 | Self::InvalidIndentationLevel { line }
162 | Self::BlankLineInGuide { line }
163 | Self::InvalidPathFormat { line, .. }
164 | Self::InvalidCommentFormat { line }
165 | Self::AdjacentPlaceholders { line }
166 | Self::PlaceholderWithChildren { line } => Some(*line),
167 Self::EmptyGuideBlock => None,
168 }
169 }
170}
171
172impl SemanticError {
173 pub fn line_number(&self) -> usize {
175 match self {
176 Self::ItemNotFound { line, .. }
177 | Self::TypeMismatch { line, .. }
178 | Self::InvalidNesting { line, .. }
179 | Self::SymlinkTargetMismatch { line, .. }
180 | Self::PermissionDenied { line, .. }
181 | Self::PlaceholderNoUnmentionedItems { line, .. } => *line,
182 }
183 }
184}
185
186pub struct ErrorFormatter;
188
189impl ErrorFormatter {
190 pub fn format_with_context(error: &AppError, file_content: Option<&str>) -> String {
192 match error {
193 AppError::Syntax(e) => {
194 if let Some(line_num) = e.line_number() {
195 Self::format_with_line_context(e.to_string(), line_num, file_content)
196 } else {
197 e.to_string()
198 }
199 }
200 AppError::Semantic(e) => {
201 Self::format_with_line_context(e.to_string(), e.line_number(), file_content)
202 }
203 _ => error.to_string(),
204 }
205 }
206
207 fn format_with_line_context(
208 error_msg: String,
209 line_num: usize,
210 file_content: Option<&str>,
211 ) -> String {
212 if let Some(content) = file_content {
213 if let Some(line) = content.lines().nth(line_num.saturating_sub(1)) {
214 format!("{}\n {}", error_msg, line.trim())
215 } else {
216 error_msg
217 }
218 } else {
219 error_msg
220 }
221 }
222}