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
93#[derive(Debug, Error, PartialEq, Eq)]
95pub enum SemanticError {
96 #[error("line {line}: {item_type} '{path}' not found at {full_path}")]
98 ItemNotFound {
99 line: usize,
100 item_type: String,
101 path: String,
102 full_path: PathBuf,
103 },
104
105 #[error("line {line}: expected {expected} but found {found} at '{path}'")]
107 TypeMismatch {
108 line: usize,
109 expected: String,
110 found: String,
111 path: String,
112 },
113
114 #[error("line {line}: '{child}' is not a child of '{parent}'")]
116 InvalidNesting {
117 line: usize,
118 child: String,
119 parent: String,
120 },
121
122 #[error("line {line}: symlink '{path}' points to '{actual}' but guide specifies '{expected}'")]
124 SymlinkTargetMismatch {
125 line: usize,
126 path: String,
127 expected: String,
128 actual: String,
129 },
130
131 #[error("line {line}: permission denied accessing '{path}'")]
133 PermissionDenied { line: usize, path: String },
134}
135
136impl SyntaxError {
137 pub fn line_number(&self) -> Option<usize> {
139 match self {
140 Self::MissingOpeningMarker { line }
141 | Self::MissingClosingMarker { line }
142 | Self::MultipleGuideBlocks { line }
143 | Self::InvalidListFormat { line }
144 | Self::DirectoryMissingSlash { line, .. }
145 | Self::InvalidSpecialDirectory { line, .. }
146 | Self::InconsistentIndentation { line, .. }
147 | Self::InvalidIndentationLevel { line }
148 | Self::BlankLineInGuide { line }
149 | Self::InvalidPathFormat { line, .. }
150 | Self::InvalidCommentFormat { line } => Some(*line),
151 Self::EmptyGuideBlock => None,
152 }
153 }
154}
155
156impl SemanticError {
157 pub fn line_number(&self) -> usize {
159 match self {
160 Self::ItemNotFound { line, .. }
161 | Self::TypeMismatch { line, .. }
162 | Self::InvalidNesting { line, .. }
163 | Self::SymlinkTargetMismatch { line, .. }
164 | Self::PermissionDenied { line, .. } => *line,
165 }
166 }
167}
168
169pub struct ErrorFormatter;
171
172impl ErrorFormatter {
173 pub fn format_with_context(error: &AppError, file_content: Option<&str>) -> String {
175 match error {
176 AppError::Syntax(e) => {
177 if let Some(line_num) = e.line_number() {
178 Self::format_with_line_context(e.to_string(), line_num, file_content)
179 } else {
180 e.to_string()
181 }
182 }
183 AppError::Semantic(e) => {
184 Self::format_with_line_context(e.to_string(), e.line_number(), file_content)
185 }
186 _ => error.to_string(),
187 }
188 }
189
190 fn format_with_line_context(
191 error_msg: String,
192 line_num: usize,
193 file_content: Option<&str>,
194 ) -> String {
195 if let Some(content) = file_content {
196 if let Some(line) = content.lines().nth(line_num.saturating_sub(1)) {
197 format!("{}\n {}", error_msg, line.trim())
198 } else {
199 error_msg
200 }
201 } else {
202 error_msg
203 }
204 }
205}