mi6_core/model/
error.rs

1//! Unified error types for mi6-core.
2//!
3//! This module consolidates all error types into a single location, providing
4//! both domain-specific errors and a unified [`Mi6Error`] type for convenient
5//! error handling.
6//!
7//! # Error Types
8//!
9//! - [`Mi6Error`]: Unified error type that wraps all domain errors
10//! - [`StorageError`]: Database and storage operations
11//! - [`ConfigError`]: Configuration file loading
12//! - [`TtlParseError`]: TTL/retention string parsing
13//! - [`InitError`]: Hook installation and initialization
14//! - [`TranscriptError`]: Transcript file parsing
15//! - [`ScanError`]: Transcript scanning with storage integration
16//!
17//! # Example
18//!
19//! ```
20//! use mi6_core::{Mi6Error, StorageError, ConfigError};
21//!
22//! fn example() -> Result<(), Mi6Error> {
23//!     // Domain errors automatically convert to Mi6Error
24//!     // via From implementations
25//!     Ok(())
26//! }
27//! ```
28
29use thiserror::Error;
30
31// =============================================================================
32// Generic error utilities
33// =============================================================================
34
35/// A boxed error type for wrapping underlying errors.
36///
37/// This is commonly used when interfacing with libraries that return
38/// different error types, allowing them to be stored uniformly.
39pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
40
41/// A simple string-based error for cases where we need to create
42/// an error from a message without an underlying error source.
43///
44/// # Example
45///
46/// ```
47/// use mi6_core::StringError;
48///
49/// let err = StringError("something went wrong".to_string());
50/// assert_eq!(err.to_string(), "something went wrong");
51/// ```
52#[derive(Debug, Clone)]
53pub struct StringError(pub String);
54
55impl std::fmt::Display for StringError {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "{}", self.0)
58    }
59}
60
61impl std::error::Error for StringError {}
62
63// =============================================================================
64// Unified Mi6Error
65// =============================================================================
66
67/// Unified error type for all mi6-core operations.
68///
69/// This enum wraps all domain-specific error types, providing a single
70/// error type that can be used throughout the library. Each variant
71/// implements `From` for convenient `?` operator usage.
72///
73/// # Example
74///
75/// ```ignore
76/// use mi6_core::{Mi6Error, Config, Storage};
77///
78/// fn init_storage() -> Result<(), Mi6Error> {
79///     let config = Config::load()?;  // ConfigError -> Mi6Error
80///     let db_path = config.db_path()?;  // ConfigError -> Mi6Error
81///     // ... storage operations would convert StorageError -> Mi6Error
82///     Ok(())
83/// }
84/// ```
85#[derive(Debug, Error)]
86pub enum Mi6Error {
87    /// Storage/database operation errors
88    #[error(transparent)]
89    Storage(#[from] StorageError),
90
91    /// Configuration loading errors
92    #[error(transparent)]
93    Config(#[from] ConfigError),
94
95    /// TTL/retention parsing errors
96    #[error(transparent)]
97    TtlParse(#[from] TtlParseError),
98
99    /// Hook installation/initialization errors
100    #[error(transparent)]
101    Init(#[from] InitError),
102
103    /// Transcript parsing errors
104    #[error(transparent)]
105    Transcript(#[from] TranscriptError),
106
107    /// Transcript scanning errors
108    #[error(transparent)]
109    Scan(#[from] ScanError),
110}
111
112// =============================================================================
113// Domain-specific errors
114// =============================================================================
115
116/// Errors from storage/database operations.
117#[derive(Error, Debug)]
118pub enum StorageError {
119    /// Failed to establish database connection
120    #[error("connection error: {0}")]
121    Connection(#[source] BoxError),
122
123    /// Failed to execute a database query
124    #[error("query error: {0}")]
125    Query(#[source] BoxError),
126
127    /// I/O error during storage operations
128    #[error("io error: {0}")]
129    Io(#[from] std::io::Error),
130}
131
132/// Errors from configuration loading.
133#[derive(Error, Debug)]
134pub enum ConfigError {
135    /// Failed to read config file
136    #[error("failed to read config file: {0}")]
137    ReadError(#[from] std::io::Error),
138
139    /// Failed to parse TOML config
140    #[error("failed to parse config file: {0}")]
141    ParseError(#[from] toml::de::Error),
142
143    /// Failed to serialize TOML config
144    #[error("failed to serialize config: {0}")]
145    TomlSerialize(String),
146
147    /// Could not determine home directory
148    #[error("failed to determine home directory")]
149    NoHomeDir,
150}
151
152/// Errors from parsing TTL/retention strings.
153#[derive(Error, Debug, PartialEq, Eq)]
154pub enum TtlParseError {
155    /// Invalid TTL format (e.g., not a recognized suffix)
156    #[error("invalid TTL format: {0}")]
157    InvalidFormat(String),
158
159    /// Invalid number in TTL string
160    #[error("invalid TTL number: {0}")]
161    InvalidNumber(String),
162}
163
164/// Errors from hook installation and initialization.
165#[derive(Debug, Error)]
166pub enum InitError {
167    /// Unknown framework name
168    #[error("unknown framework: {0}")]
169    UnknownFramework(String),
170
171    /// Failed to determine settings path
172    #[error("failed to determine settings path: {0}")]
173    SettingsPath(String),
174
175    /// Failed to read or write settings file
176    #[error("settings file error: {0}")]
177    SettingsFile(#[from] std::io::Error),
178
179    /// Failed to parse or serialize configuration
180    #[error("configuration error: {0}")]
181    Config(String),
182}
183
184/// Errors from transcript file parsing.
185#[derive(Debug, Error)]
186pub enum TranscriptError {
187    /// I/O error reading transcript
188    #[error("IO error: {0}")]
189    Io(#[from] std::io::Error),
190
191    /// JSON parse error at specific line
192    #[error("JSON parse error at line {line}: {message}")]
193    Parse {
194        /// Line number where error occurred
195        line: u64,
196        /// Error message
197        message: String,
198    },
199
200    /// Transcript file not found
201    #[error("File not found: {0}")]
202    NotFound(String),
203}
204
205/// Errors from transcript scanning with storage integration.
206#[derive(Error, Debug)]
207pub enum ScanError {
208    /// Error reading/parsing the transcript file
209    #[error("transcript parse error: {0}")]
210    Parse(#[from] TranscriptError),
211
212    /// Error interacting with storage
213    #[error("storage error: {0}")]
214    Storage(#[from] StorageError),
215}
216
217/// Errors from framework resolution.
218#[derive(Debug, Error)]
219pub enum FrameworkResolutionError {
220    /// Unknown framework name
221    #[error("unknown framework: {0}")]
222    UnknownFramework(String),
223
224    /// No frameworks found (auto-detection failed)
225    #[error("{0}")]
226    NoFrameworksFound(String),
227}
228
229// =============================================================================
230// Tests
231// =============================================================================
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_string_error_display() {
239        let msg = "test error message";
240        let err = StringError(msg.to_string());
241        assert_eq!(err.to_string(), msg);
242    }
243
244    #[test]
245    fn test_string_error_is_error() {
246        let err = StringError("test".to_string());
247        // Verify it implements std::error::Error
248        let _: &dyn std::error::Error = &err;
249    }
250
251    #[test]
252    fn test_string_error_clone() {
253        let err = StringError("cloneable".to_string());
254        let cloned = err.clone();
255        assert_eq!(err.0, cloned.0);
256    }
257
258    #[test]
259    fn test_box_error_from_string_error() {
260        let err = StringError("boxed".to_string());
261        let boxed: BoxError = Box::new(err);
262        assert_eq!(boxed.to_string(), "boxed");
263    }
264
265    #[test]
266    fn test_mi6_error_from_storage_error() {
267        let storage_err = StorageError::Io(std::io::Error::new(
268            std::io::ErrorKind::NotFound,
269            "not found",
270        ));
271        let mi6_err: Mi6Error = storage_err.into();
272        assert!(matches!(mi6_err, Mi6Error::Storage(_)));
273    }
274
275    #[test]
276    fn test_mi6_error_from_config_error() {
277        let config_err = ConfigError::NoHomeDir;
278        let mi6_err: Mi6Error = config_err.into();
279        assert!(matches!(mi6_err, Mi6Error::Config(_)));
280    }
281
282    #[test]
283    fn test_mi6_error_from_ttl_parse_error() {
284        let ttl_err = TtlParseError::InvalidFormat("bad".to_string());
285        let mi6_err: Mi6Error = ttl_err.into();
286        assert!(matches!(mi6_err, Mi6Error::TtlParse(_)));
287    }
288
289    #[test]
290    fn test_mi6_error_from_init_error() {
291        let init_err = InitError::UnknownFramework("test".to_string());
292        let mi6_err: Mi6Error = init_err.into();
293        assert!(matches!(mi6_err, Mi6Error::Init(_)));
294    }
295
296    #[test]
297    fn test_mi6_error_from_transcript_error() {
298        let transcript_err = TranscriptError::NotFound("file.json".to_string());
299        let mi6_err: Mi6Error = transcript_err.into();
300        assert!(matches!(mi6_err, Mi6Error::Transcript(_)));
301    }
302
303    #[test]
304    fn test_mi6_error_from_scan_error() {
305        let scan_err = ScanError::Parse(TranscriptError::NotFound("file.json".to_string()));
306        let mi6_err: Mi6Error = scan_err.into();
307        assert!(matches!(mi6_err, Mi6Error::Scan(_)));
308    }
309
310    #[test]
311    fn test_mi6_error_display_transparent() {
312        let storage_err = StorageError::Query(Box::new(StringError("query failed".to_string())));
313        let mi6_err: Mi6Error = storage_err.into();
314        // #[error(transparent)] means it displays the inner error's message
315        assert!(mi6_err.to_string().contains("query failed"));
316    }
317
318    #[test]
319    fn test_storage_error_source_preserved() {
320        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
321        let storage_err = StorageError::Connection(Box::new(io_err));
322
323        use std::error::Error;
324        let source = storage_err.source();
325        assert!(source.is_some());
326        assert!(source.is_some_and(|s| s.to_string().contains("file not found")));
327    }
328}