agentic_navigation_guide/
errors.rs

1//! Error types for the agentic navigation guide
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6/// Result type alias for the library
7pub type Result<T> = std::result::Result<T, AppError>;
8
9/// Top-level application error type
10#[derive(Debug, Error)]
11pub enum AppError {
12    /// Syntax error in the navigation guide
13    #[error("syntax error: {0}")]
14    Syntax(#[from] SyntaxError),
15
16    /// Semantic error when verifying against filesystem
17    #[error("semantic error: {0}")]
18    Semantic(#[from] SemanticError),
19
20    /// I/O error
21    #[error("I/O error: {0}")]
22    Io(#[from] std::io::Error),
23
24    /// Pattern parsing error
25    #[error("invalid glob pattern: {0}")]
26    GlobPattern(#[from] globset::Error),
27
28    /// WalkDir error
29    #[error("filesystem walk error: {0}")]
30    WalkDir(#[from] walkdir::Error),
31
32    /// Other errors
33    #[error("{0}")]
34    Other(String),
35}
36
37/// Syntax errors in navigation guide format
38#[derive(Debug, Error, PartialEq, Eq)]
39pub enum SyntaxError {
40    /// Missing opening sentinel marker
41    #[error("line {line}: missing opening <agentic-navigation-guide> marker")]
42    MissingOpeningMarker { line: usize },
43
44    /// Missing closing sentinel marker
45    #[error("line {line}: missing closing </agentic-navigation-guide> marker")]
46    MissingClosingMarker { line: usize },
47
48    /// Multiple guide blocks found
49    #[error("line {line}: multiple <agentic-navigation-guide> blocks found")]
50    MultipleGuideBlocks { line: usize },
51
52    /// Empty guide block
53    #[error("empty navigation guide block")]
54    EmptyGuideBlock,
55
56    /// Invalid list format
57    #[error("line {line}: invalid list format - must start with '-'")]
58    InvalidListFormat { line: usize },
59
60    /// Directory missing trailing slash
61    #[error("line {line}: directory '{path}' must end with '/'")]
62    DirectoryMissingSlash { line: usize, path: String },
63
64    /// Invalid special directory
65    #[error("line {line}: invalid special directory '{path}' (. and .. are not allowed)")]
66    InvalidSpecialDirectory { line: usize, path: String },
67
68    /// Inconsistent indentation
69    #[error("line {line}: inconsistent indentation - expected {expected} spaces, found {found}")]
70    InconsistentIndentation {
71        line: usize,
72        expected: usize,
73        found: usize,
74    },
75
76    /// Invalid indentation level
77    #[error("line {line}: invalid indentation level - must be a multiple of the indent size")]
78    InvalidIndentationLevel { line: usize },
79
80    /// Blank line in guide block
81    #[error("line {line}: blank lines are not allowed within the guide block")]
82    BlankLineInGuide { line: usize },
83
84    /// Invalid path format
85    #[error("line {line}: invalid path format '{path}'")]
86    InvalidPathFormat { line: usize, path: String },
87
88    /// Invalid comment format
89    #[error("line {line}: invalid comment format - comments must be separated by '#'")]
90    InvalidCommentFormat { line: usize },
91
92    /// Adjacent placeholders
93    #[error("line {line}: placeholder entries (...) cannot be adjacent to each other")]
94    AdjacentPlaceholders { line: usize },
95
96    /// Placeholder with children
97    #[error("line {line}: placeholder entries (...) cannot have child elements")]
98    PlaceholderWithChildren { line: usize },
99}
100
101/// Semantic errors when verifying against filesystem
102#[derive(Debug, Error, PartialEq, Eq)]
103pub enum SemanticError {
104    /// File or directory not found
105    #[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    /// Type mismatch (file vs directory)
114    #[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    /// Invalid nesting - item not actually child of parent
123    #[error("line {line}: '{child}' is not a child of '{parent}'")]
124    InvalidNesting {
125        line: usize,
126        child: String,
127        parent: String,
128    },
129
130    /// Symlink target mismatch
131    #[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    /// Permission denied accessing item
140    #[error("line {line}: permission denied accessing '{path}'")]
141    PermissionDenied { line: usize, path: String },
142
143    /// Placeholder with no unmentioned items
144    #[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    /// Get the line number associated with this error, if any
152    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    /// Get the line number associated with this error
174    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
186/// Format errors for display with optional context
187pub struct ErrorFormatter;
188
189impl ErrorFormatter {
190    /// Format an error with line context if available
191    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}