use std::path::PathBuf;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, AppError>;
#[derive(Debug, Error)]
pub enum AppError {
#[error("syntax error: {0}")]
Syntax(#[from] SyntaxError),
#[error("semantic error: {0}")]
Semantic(#[from] SemanticError),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("invalid glob pattern: {0}")]
GlobPattern(#[from] globset::Error),
#[error("filesystem walk error: {0}")]
WalkDir(#[from] walkdir::Error),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum SyntaxError {
#[error("line {line}: missing opening <agentic-navigation-guide> marker")]
MissingOpeningMarker { line: usize },
#[error("line {line}: missing closing </agentic-navigation-guide> marker")]
MissingClosingMarker { line: usize },
#[error("line {line}: multiple <agentic-navigation-guide> blocks found")]
MultipleGuideBlocks { line: usize },
#[error("empty navigation guide block")]
EmptyGuideBlock,
#[error("line {line}: invalid list format - must start with '-'")]
InvalidListFormat { line: usize },
#[error("line {line}: directory '{path}' must end with '/'")]
DirectoryMissingSlash { line: usize, path: String },
#[error("line {line}: invalid special directory '{path}' (. and .. are not allowed)")]
InvalidSpecialDirectory { line: usize, path: String },
#[error("line {line}: inconsistent indentation - expected {expected} spaces, found {found}")]
InconsistentIndentation {
line: usize,
expected: usize,
found: usize,
},
#[error("line {line}: invalid indentation level - must be a multiple of the indent size")]
InvalidIndentationLevel { line: usize },
#[error("line {line}: blank lines are not allowed within the guide block")]
BlankLineInGuide { line: usize },
#[error("line {line}: invalid path format '{path}'")]
InvalidPathFormat { line: usize, path: String },
#[error("line {line}: invalid wildcard choice syntax in '{path}': {message}")]
InvalidWildcardSyntax {
line: usize,
path: String,
message: String,
},
#[error("line {line}: invalid comment format - comments must be separated by '#'")]
InvalidCommentFormat { line: usize },
#[error("line {line}: placeholder entries (...) cannot be adjacent to each other")]
AdjacentPlaceholders { line: usize },
#[error("line {line}: placeholder entries (...) cannot have child elements")]
PlaceholderWithChildren { line: usize },
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum SemanticError {
#[error("line {line}: {item_type} '{path}' not found at {full_path}")]
ItemNotFound {
line: usize,
item_type: String,
path: String,
full_path: PathBuf,
},
#[error("line {line}: expected {expected} but found {found} at '{path}'")]
TypeMismatch {
line: usize,
expected: String,
found: String,
path: String,
},
#[error("line {line}: '{child}' is not a child of '{parent}'")]
InvalidNesting {
line: usize,
child: String,
parent: String,
},
#[error("line {line}: symlink '{path}' points to '{actual}' but guide specifies '{expected}'")]
SymlinkTargetMismatch {
line: usize,
path: String,
expected: String,
actual: String,
},
#[error("line {line}: permission denied accessing '{path}'")]
PermissionDenied { line: usize, path: String },
#[error(
"line {line}: placeholder (...) must refer to at least one unlisted item in '{parent}'"
)]
PlaceholderNoUnmentionedItems { line: usize, parent: String },
}
impl SyntaxError {
pub fn line_number(&self) -> Option<usize> {
match self {
Self::MissingOpeningMarker { line }
| Self::MissingClosingMarker { line }
| Self::MultipleGuideBlocks { line }
| Self::InvalidListFormat { line }
| Self::DirectoryMissingSlash { line, .. }
| Self::InvalidSpecialDirectory { line, .. }
| Self::InconsistentIndentation { line, .. }
| Self::InvalidIndentationLevel { line }
| Self::BlankLineInGuide { line }
| Self::InvalidPathFormat { line, .. }
| Self::InvalidWildcardSyntax { line, .. }
| Self::InvalidCommentFormat { line }
| Self::AdjacentPlaceholders { line }
| Self::PlaceholderWithChildren { line } => Some(*line),
Self::EmptyGuideBlock => None,
}
}
}
impl SemanticError {
pub fn line_number(&self) -> usize {
match self {
Self::ItemNotFound { line, .. }
| Self::TypeMismatch { line, .. }
| Self::InvalidNesting { line, .. }
| Self::SymlinkTargetMismatch { line, .. }
| Self::PermissionDenied { line, .. }
| Self::PlaceholderNoUnmentionedItems { line, .. } => *line,
}
}
}
pub struct ErrorFormatter;
impl ErrorFormatter {
pub fn format_with_context(error: &AppError, file_content: Option<&str>) -> String {
match error {
AppError::Syntax(e) => {
if let Some(line_num) = e.line_number() {
Self::format_with_line_context(e.to_string(), line_num, file_content)
} else {
e.to_string()
}
}
AppError::Semantic(e) => {
Self::format_with_line_context(e.to_string(), e.line_number(), file_content)
}
_ => error.to_string(),
}
}
fn format_with_line_context(
error_msg: String,
line_num: usize,
file_content: Option<&str>,
) -> String {
if let Some(content) = file_content {
if let Some(line) = content.lines().nth(line_num.saturating_sub(1)) {
format!("{}\n {}", error_msg, line.trim())
} else {
error_msg
}
} else {
error_msg
}
}
}