Skip to main content

sara_core/
error.rs

1//! Error types for the sara-core library.
2//!
3//! This module defines a unified error type for all SARA operations.
4//! All errors are consolidated into the single [`SaraError`] enum with
5//! clear variants for each error category.
6//!
7//! # Error Categories
8//!
9//! - **File Operations**: Reading and writing files
10//! - **Parsing**: Markdown and YAML frontmatter parsing
11//! - **Validation**: Graph structure and item validation
12//! - **Configuration**: Loading and validating configuration
13//! - **Queries**: Item lookup and graph traversal
14//! - **Git Operations**: Repository access and version control
15//! - **Editing**: Item modification operations
16//!
17//! # Examples
18//!
19//! ```
20//! use sara_core::error::SaraError;
21//! use std::path::PathBuf;
22//!
23//! # fn example() -> Result<(), SaraError> {
24//! // Validation errors use explicit variants
25//! let err = SaraError::BrokenReference {
26//!     from: sara_core::model::ItemId::new_unchecked("UC-001"),
27//!     to: sara_core::model::ItemId::new_unchecked("SOL-999"),
28//! };
29//!
30//! // File operations with context
31//! let err = SaraError::InvalidFrontmatter {
32//!     file: PathBuf::from("doc.md"),
33//!     reason: "Missing required 'id' field".to_string(),
34//! };
35//! # Ok(())
36//! # }
37//! ```
38
39use std::path::PathBuf;
40use thiserror::Error;
41
42use crate::model::{ItemId, ItemType, RelationshipType};
43
44/// Main error type for sara-core operations.
45///
46/// Consolidates all error categories into a single type with clear variants.
47/// Uses `thiserror` for automatic `Display` and `Error` trait implementations.
48///
49/// # Errors
50///
51/// This enum categorizes all possible errors that can occur during SARA operations.
52/// Each variant includes contextual information to help diagnose the issue.
53///
54/// # Examples
55///
56/// ```
57/// use sara_core::error::SaraError;
58/// use std::path::PathBuf;
59///
60/// # fn example() -> Result<(), SaraError> {
61/// // File not found
62/// let err = SaraError::FileNotFound { path: PathBuf::from("missing.md") };
63///
64/// // Parse error with context
65/// let err = SaraError::InvalidFrontmatter {
66///     file: PathBuf::from("doc.md"),
67///     reason: "Missing 'id' field".to_string(),
68/// };
69/// # Ok(())
70/// # }
71/// ```
72#[derive(Debug, Error, serde::Serialize)]
73#[serde(tag = "error_type", content = "details")]
74pub enum SaraError {
75    // ==================== File Operations ====================
76    /// Failed to read a file from the filesystem.
77    #[error("Failed to read file '{path}': {source}")]
78    FileRead {
79        /// Path to the file that couldn't be read.
80        path: PathBuf,
81        /// Underlying I/O error.
82        #[serde(skip)]
83        #[source]
84        source: std::io::Error,
85    },
86
87    /// File was not found at the specified path.
88    #[error("File not found: {path}")]
89    FileNotFound {
90        /// Path to the missing file.
91        path: PathBuf,
92    },
93
94    /// Failed to write a file to the filesystem.
95    #[error("Failed to write file '{path}': {source}")]
96    FileWrite {
97        /// Path to the file that couldn't be written.
98        path: PathBuf,
99        /// Underlying I/O error.
100        #[serde(skip)]
101        #[source]
102        source: std::io::Error,
103    },
104
105    // ==================== Parsing ====================
106    /// Invalid frontmatter in a Markdown file.
107    #[error("Invalid frontmatter in {file}: {reason}")]
108    InvalidFrontmatter {
109        /// Path to the file with invalid frontmatter.
110        file: PathBuf,
111        /// Description of what's wrong with the frontmatter.
112        reason: String,
113    },
114
115    /// File is missing required frontmatter section.
116    #[error("Missing frontmatter in {file}")]
117    MissingFrontmatter {
118        /// Path to the file without frontmatter.
119        file: PathBuf,
120    },
121
122    /// Invalid YAML syntax in frontmatter.
123    #[error("Invalid YAML in {file}: {reason}")]
124    InvalidYaml {
125        /// Path to the file with invalid YAML.
126        file: PathBuf,
127        /// YAML parsing error details.
128        reason: String,
129    },
130
131    /// Invalid item type value in frontmatter.
132    #[error("Invalid item type '{value}' in {file}")]
133    InvalidItemType {
134        /// Path to the file with the invalid type.
135        file: PathBuf,
136        /// The invalid type value encountered.
137        value: String,
138    },
139
140    /// Missing required field in frontmatter.
141    #[error("Missing required field '{field}' in {file}")]
142    MissingField {
143        /// The field that was missing.
144        field: String,
145        /// Path to the file.
146        file: PathBuf,
147    },
148
149    // ==================== Validation ====================
150    /// Invalid item ID format.
151    #[error("Invalid item ID '{id}': {reason}")]
152    InvalidId {
153        /// The invalid ID.
154        id: String,
155        /// Why the ID is invalid.
156        reason: String,
157    },
158
159    /// Broken reference to non-existent item.
160    #[error("Broken reference: {from} references non-existent item {to}")]
161    BrokenReference {
162        /// The item with the broken reference.
163        from: ItemId,
164        /// The non-existent item being referenced.
165        to: ItemId,
166    },
167
168    /// Orphan item with no upstream parent.
169    #[error("Orphan item: {id} ({item_type}) has no upstream parent")]
170    OrphanItem {
171        /// The orphaned item ID.
172        id: ItemId,
173        /// The item type.
174        item_type: ItemType,
175    },
176
177    /// Duplicate identifier found in multiple files.
178    #[error("Duplicate identifier: {id} defined in multiple files")]
179    DuplicateIdentifier {
180        /// The duplicated ID.
181        id: ItemId,
182    },
183
184    /// Circular reference detected in the graph.
185    #[error("Circular reference detected: {cycle}")]
186    CircularReference {
187        /// Description of the cycle.
188        cycle: String,
189    },
190
191    /// Invalid relationship between item types.
192    #[error("Invalid relationship: {from_id} ({from_type}) cannot {rel_type} {to_id} ({to_type})")]
193    InvalidRelationship {
194        /// Source item ID.
195        from_id: ItemId,
196        /// Target item ID.
197        to_id: ItemId,
198        /// Source item type.
199        from_type: ItemType,
200        /// Target item type.
201        to_type: ItemType,
202        /// Relationship type attempted.
203        rel_type: RelationshipType,
204    },
205
206    /// Invalid metadata in item.
207    #[error("Invalid metadata in {file}: {reason}")]
208    InvalidMetadata {
209        /// File containing the invalid metadata.
210        file: String,
211        /// Description of the metadata issue.
212        reason: String,
213    },
214
215    /// Unrecognized field in frontmatter.
216    #[error("Unrecognized field '{field}' in {file}")]
217    UnrecognizedField {
218        /// The unrecognized field name.
219        field: String,
220        /// File containing the field.
221        file: String,
222    },
223
224    /// Redundant relationship declared on both sides.
225    #[error(
226        "Redundant relationship: {from_id} and {to_id} both declare the relationship (only one is needed)"
227    )]
228    RedundantRelationship {
229        /// First item ID.
230        from_id: ItemId,
231        /// Second item ID.
232        to_id: ItemId,
233    },
234
235    // ==================== Configuration ====================
236    /// Configuration file could not be read.
237    #[error("Failed to read config file {path}: {reason}")]
238    ConfigRead {
239        /// Path to the config file.
240        path: PathBuf,
241        /// Reason for the failure.
242        reason: String,
243    },
244
245    /// Configuration file has invalid content.
246    #[error("Invalid config file {path}: {reason}")]
247    InvalidConfig {
248        /// Path to the config file.
249        path: PathBuf,
250        /// Description of the configuration error.
251        reason: String,
252    },
253
254    /// Repository path does not exist or is not a directory.
255    #[error("Repository not found: {path}")]
256    RepositoryNotFound {
257        /// Path that was expected to be a repository.
258        path: PathBuf,
259    },
260
261    // ==================== Queries ====================
262    /// No parent items exist for the given item type.
263    #[error(
264        "Cannot create {item_type}: no {parent_type} items exist. Create a {parent_type} first."
265    )]
266    MissingParent {
267        /// The item type that requires a parent.
268        item_type: String,
269        /// The parent type that is missing.
270        parent_type: String,
271    },
272
273    /// Item was not found in the knowledge graph.
274    #[error("Item not found: {id}")]
275    ItemNotFound {
276        /// The item ID that wasn't found.
277        id: String,
278        /// Suggested similar item IDs (fuzzy matches).
279        suggestions: Vec<String>,
280    },
281
282    /// Query syntax or parameters are invalid.
283    #[error("Invalid query: {reason}")]
284    InvalidQuery {
285        /// Description of what's wrong with the query.
286        reason: String,
287    },
288
289    // ==================== Git Operations ====================
290    /// Failed to open a Git repository.
291    #[error("Failed to open repository {path}: {reason}")]
292    GitOpenRepository {
293        /// Path to the repository.
294        path: PathBuf,
295        /// Error from git2.
296        reason: String,
297    },
298
299    /// Git reference (branch, tag, commit) is invalid.
300    #[error("Invalid Git reference: {reference}")]
301    InvalidGitReference {
302        /// The invalid reference string.
303        reference: String,
304    },
305
306    /// Failed to read a file from a Git reference.
307    #[error("Failed to read file {path} at {reference}: {reason}")]
308    GitReadFile {
309        /// Path to the file in the repository.
310        path: PathBuf,
311        /// Git reference (commit, branch, tag).
312        reference: String,
313        /// Error details.
314        reason: String,
315    },
316
317    /// Generic Git operation error.
318    #[error("Git operation failed: {0}")]
319    Git(String),
320
321    // ==================== Edit Operations ====================
322    /// Interactive terminal required but not available.
323    #[error(
324        "Interactive mode requires a terminal. Use modification flags (--name, --description, etc.) to edit non-interactively."
325    )]
326    NonInteractiveTerminal,
327
328    /// User cancelled the operation.
329    #[error("User cancelled")]
330    Cancelled,
331
332    /// Traceability link points to non-existent item.
333    #[error("Invalid traceability link: {id} does not exist")]
334    InvalidLink {
335        /// The invalid item ID.
336        id: String,
337    },
338
339    /// Edit operation failed with custom error message.
340    #[error("Edit failed: {0}")]
341    EditFailed(String),
342
343    // ==================== Wrapped Errors ====================
344    /// Standard I/O error.
345    #[error("I/O error: {0}")]
346    Io(
347        #[serde(skip)]
348        #[from]
349        std::io::Error,
350    ),
351
352    /// Git2 library error.
353    #[error("Git error: {0}")]
354    Git2(
355        #[serde(skip)]
356        #[from]
357        git2::Error,
358    ),
359}
360
361impl SaraError {
362    /// Formats suggestions as a user-friendly message.
363    ///
364    /// Returns `None` if this is not an `ItemNotFound` error or if there are no suggestions.
365    pub fn format_suggestions(&self) -> Option<String> {
366        match self {
367            Self::ItemNotFound { suggestions, .. } if !suggestions.is_empty() => {
368                Some(format!("Did you mean: {}?", suggestions.join(", ")))
369            }
370            _ => None,
371        }
372    }
373}
374
375/// Result type for sara-core operations.
376///
377/// This is a convenience alias for `Result<T, SaraError>`.
378///
379/// # Examples
380///
381/// ```
382/// use sara_core::error::Result;
383///
384/// fn parse_file() -> Result<String> {
385///     Ok("parsed content".to_string())
386/// }
387/// ```
388pub type Result<T> = std::result::Result<T, SaraError>;
389
390// Rust guideline compliant 2026-02-06