use std::path::PathBuf;
use thiserror::Error;
use crate::model::{ItemId, ItemType, RelationshipType};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ValidationErrorCode {
InvalidId,
MissingField,
BrokenReference,
OrphanItem,
DuplicateIdentifier,
CircularReference,
InvalidRelationship,
InvalidMetadata,
UnrecognizedField,
RedundantRelationship,
}
impl ValidationErrorCode {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::InvalidId => "invalid_id",
Self::MissingField => "missing_field",
Self::BrokenReference => "broken_reference",
Self::OrphanItem => "orphan_item",
Self::DuplicateIdentifier => "duplicate_identifier",
Self::CircularReference => "circular_reference",
Self::InvalidRelationship => "invalid_relationship",
Self::InvalidMetadata => "invalid_metadata",
Self::UnrecognizedField => "unrecognized_field",
Self::RedundantRelationship => "redundant_relationship",
}
}
}
impl std::fmt::Display for ValidationErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Error)]
pub enum ParseError {
#[error("Failed to read file {path}: {reason}")]
FileRead { path: PathBuf, reason: String },
#[error("Invalid frontmatter in {file}: {reason}")]
InvalidFrontmatter { file: PathBuf, reason: String },
#[error("Missing frontmatter in {file}")]
MissingFrontmatter { file: PathBuf },
#[error("Invalid YAML in {file}: {reason}")]
InvalidYaml { file: PathBuf, reason: String },
#[error("Missing required field '{field}' in {file}")]
MissingField { file: PathBuf, field: String },
#[error("Invalid item type '{value}' in {file}")]
InvalidItemType { file: PathBuf, value: String },
}
#[derive(Debug, Error, Clone, serde::Serialize)]
pub enum ValidationError {
#[error("Invalid item ID '{id}': {reason}")]
InvalidId { id: String, reason: String },
#[error("Missing required field '{field}' in {file}")]
MissingField { field: String, file: String },
#[error("Broken reference: {from} references non-existent item {to}")]
BrokenReference { from: ItemId, to: ItemId },
#[error("Orphan item: {id} has no upstream parent")]
OrphanItem { id: ItemId, item_type: ItemType },
#[error("Duplicate identifier: {id} defined in multiple files")]
DuplicateIdentifier { id: ItemId },
#[error("Circular reference detected: {cycle}")]
CircularReference { cycle: String },
#[error("Invalid relationship: {from_type} cannot {rel_type} {to_type}")]
InvalidRelationship {
from_id: ItemId,
to_id: ItemId,
from_type: ItemType,
to_type: ItemType,
rel_type: RelationshipType,
},
#[error("Invalid metadata in {file}: {reason}")]
InvalidMetadata { file: String, reason: String },
#[error("Unrecognized field '{field}' in {file}")]
UnrecognizedField { field: String, file: String },
#[error(
"Redundant relationship: {from_id} and {to_id} both declare the relationship (only one is needed)"
)]
RedundantRelationship {
from_id: ItemId,
to_id: ItemId,
from_rel: RelationshipType,
to_rel: RelationshipType,
},
}
impl ValidationError {
#[must_use]
pub const fn is_error(&self) -> bool {
!matches!(
self,
Self::UnrecognizedField { .. } | Self::RedundantRelationship { .. }
)
}
#[must_use]
pub const fn code(&self) -> ValidationErrorCode {
match self {
Self::InvalidId { .. } => ValidationErrorCode::InvalidId,
Self::MissingField { .. } => ValidationErrorCode::MissingField,
Self::BrokenReference { .. } => ValidationErrorCode::BrokenReference,
Self::OrphanItem { .. } => ValidationErrorCode::OrphanItem,
Self::DuplicateIdentifier { .. } => ValidationErrorCode::DuplicateIdentifier,
Self::CircularReference { .. } => ValidationErrorCode::CircularReference,
Self::InvalidRelationship { .. } => ValidationErrorCode::InvalidRelationship,
Self::InvalidMetadata { .. } => ValidationErrorCode::InvalidMetadata,
Self::UnrecognizedField { .. } => ValidationErrorCode::UnrecognizedField,
Self::RedundantRelationship { .. } => ValidationErrorCode::RedundantRelationship,
}
}
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Failed to read config file {path}: {reason}")]
FileRead { path: PathBuf, reason: String },
#[error("Invalid config file {path}: {reason}")]
InvalidConfig { path: PathBuf, reason: String },
#[error("Repository not found: {path}")]
RepositoryNotFound { path: PathBuf },
#[error("Invalid glob pattern '{pattern}': {reason}")]
InvalidGlobPattern { pattern: String, reason: String },
}
#[derive(Debug, Error)]
pub enum QueryError {
#[error("Item not found: {id}")]
ItemNotFound {
id: String,
suggestions: Vec<String>,
},
#[error("Invalid query: {reason}")]
InvalidQuery { reason: String },
}
#[derive(Debug, Error)]
pub enum GitError {
#[error("Failed to open repository {path}: {reason}")]
OpenRepository { path: PathBuf, reason: String },
#[error("Invalid Git reference: {reference}")]
InvalidReference { reference: String },
#[error("Failed to read file {path} at {reference}: {reason}")]
ReadFile {
path: PathBuf,
reference: String,
reason: String,
},
}
#[derive(Debug, Error)]
pub enum SaraError {
#[error(transparent)]
Parse(#[from] ParseError),
#[error(transparent)]
Validation(Box<ValidationError>),
#[error(transparent)]
Config(#[from] ConfigError),
#[error(transparent)]
Query(#[from] QueryError),
#[error(transparent)]
Git(#[from] GitError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Git operation failed: {0}")]
GitError(String),
}
impl From<ValidationError> for SaraError {
fn from(err: ValidationError) -> Self {
SaraError::Validation(Box::new(err))
}
}
#[derive(Debug, Error)]
pub enum EditError {
#[error("Item not found: {id}")]
ItemNotFound {
id: String,
suggestions: Vec<String>,
},
#[error(
"Interactive mode requires a terminal. Use modification flags (--name, --description, etc.) to edit non-interactively."
)]
NonInteractiveTerminal,
#[error("User cancelled")]
Cancelled,
#[error("Invalid traceability link: {id} does not exist")]
InvalidLink { id: String },
#[error("Failed to read file: {0}")]
IoError(String),
#[error("Failed to parse graph: {0}")]
GraphError(String),
}
impl EditError {
pub fn format_suggestions(&self) -> Option<String> {
if let EditError::ItemNotFound { suggestions, .. } = self
&& !suggestions.is_empty()
{
return Some(format!("Did you mean: {}?", suggestions.join(", ")));
}
None
}
pub fn has_suggestions(&self) -> bool {
matches!(self, EditError::ItemNotFound { suggestions, .. } if !suggestions.is_empty())
}
}
pub type Result<T> = std::result::Result<T, SaraError>;