use std::path::PathBuf;
use thiserror::Error;
use crate::model::{ItemId, ItemType, RelationshipType};
#[derive(Debug, Error, serde::Serialize)]
#[serde(tag = "error_type", content = "details")]
pub enum SaraError {
#[error("Failed to read file '{path}': {source}")]
FileRead {
path: PathBuf,
#[serde(skip)]
#[source]
source: std::io::Error,
},
#[error("File not found: {path}")]
FileNotFound {
path: PathBuf,
},
#[error("Failed to write file '{path}': {source}")]
FileWrite {
path: PathBuf,
#[serde(skip)]
#[source]
source: std::io::Error,
},
#[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("Invalid item type '{value}' in {file}")]
InvalidItemType {
file: PathBuf,
value: String,
},
#[error("Missing required field '{field}' in {file}")]
MissingField {
field: String,
file: PathBuf,
},
#[error("Invalid item ID '{id}': {reason}")]
InvalidId {
id: String,
reason: String,
},
#[error("Broken reference: {from} references non-existent item {to}")]
BrokenReference {
from: ItemId,
to: ItemId,
},
#[error("Orphan item: {id} ({item_type}) 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_id} ({from_type}) cannot {rel_type} {to_id} ({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,
},
#[error("Failed to read config file {path}: {reason}")]
ConfigRead {
path: PathBuf,
reason: String,
},
#[error("Invalid config file {path}: {reason}")]
InvalidConfig {
path: PathBuf,
reason: String,
},
#[error("Repository not found: {path}")]
RepositoryNotFound {
path: PathBuf,
},
#[error(
"Cannot create {item_type}: no {parent_type} items exist. Create a {parent_type} first."
)]
MissingParent {
item_type: String,
parent_type: String,
},
#[error("Item not found: {id}")]
ItemNotFound {
id: String,
suggestions: Vec<String>,
},
#[error("Invalid query: {reason}")]
InvalidQuery {
reason: String,
},
#[error("Failed to open repository {path}: {reason}")]
GitOpenRepository {
path: PathBuf,
reason: String,
},
#[error("Invalid Git reference: {reference}")]
InvalidGitReference {
reference: String,
},
#[error("Failed to read file {path} at {reference}: {reason}")]
GitReadFile {
path: PathBuf,
reference: String,
reason: String,
},
#[error("Git operation failed: {0}")]
Git(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("Edit failed: {0}")]
EditFailed(String),
#[error("I/O error: {0}")]
Io(
#[serde(skip)]
#[from]
std::io::Error,
),
#[error("Git error: {0}")]
Git2(
#[serde(skip)]
#[from]
git2::Error,
),
}
impl SaraError {
pub fn format_suggestions(&self) -> Option<String> {
match self {
Self::ItemNotFound { suggestions, .. } if !suggestions.is_empty() => {
Some(format!("Did you mean: {}?", suggestions.join(", ")))
}
_ => None,
}
}
}
pub type Result<T> = std::result::Result<T, SaraError>;