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    /// Settings file contains invalid syntax (JSON or TOML)
184    #[error("{path} contains invalid {format}: {error}")]
185    InvalidSettings {
186        /// Path to the settings file
187        path: std::path::PathBuf,
188        /// Format of the settings file (e.g., "JSON" or "TOML")
189        format: &'static str,
190        /// The parse error message
191        error: String,
192    },
193}
194
195/// Errors from transcript file parsing.
196#[derive(Debug, Error)]
197pub enum TranscriptError {
198    /// I/O error reading transcript
199    #[error("IO error: {0}")]
200    Io(#[from] std::io::Error),
201
202    /// JSON parse error at specific line
203    #[error("JSON parse error at line {line}: {message}")]
204    Parse {
205        /// Line number where error occurred
206        line: u64,
207        /// Error message
208        message: String,
209    },
210
211    /// Transcript file not found
212    #[error("File not found: {0}")]
213    NotFound(String),
214}
215
216/// Errors from transcript scanning with storage integration.
217#[derive(Error, Debug)]
218pub enum ScanError {
219    /// Error reading/parsing the transcript file
220    #[error("transcript parse error: {0}")]
221    Parse(#[from] TranscriptError),
222
223    /// Error interacting with storage
224    #[error("storage error: {0}")]
225    Storage(#[from] StorageError),
226}
227
228/// Errors from framework resolution.
229#[derive(Debug, Error)]
230pub enum FrameworkResolutionError {
231    /// Unknown framework name
232    #[error("unknown framework: {0}")]
233    UnknownFramework(String),
234
235    /// No frameworks found (auto-detection failed)
236    #[error("{0}")]
237    NoFrameworksFound(String),
238}
239
240// =============================================================================
241// Tests
242// =============================================================================
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_string_error_display() {
250        let msg = "test error message";
251        let err = StringError(msg.to_string());
252        assert_eq!(err.to_string(), msg);
253    }
254
255    #[test]
256    fn test_string_error_is_error() {
257        let err = StringError("test".to_string());
258        // Verify it implements std::error::Error
259        let _: &dyn std::error::Error = &err;
260    }
261
262    #[test]
263    fn test_string_error_clone() {
264        let err = StringError("cloneable".to_string());
265        let cloned = err.clone();
266        assert_eq!(err.0, cloned.0);
267    }
268
269    #[test]
270    fn test_box_error_from_string_error() {
271        let err = StringError("boxed".to_string());
272        let boxed: BoxError = Box::new(err);
273        assert_eq!(boxed.to_string(), "boxed");
274    }
275
276    #[test]
277    fn test_mi6_error_from_storage_error() {
278        let storage_err = StorageError::Io(std::io::Error::new(
279            std::io::ErrorKind::NotFound,
280            "not found",
281        ));
282        let mi6_err: Mi6Error = storage_err.into();
283        assert!(matches!(mi6_err, Mi6Error::Storage(_)));
284    }
285
286    #[test]
287    fn test_mi6_error_from_config_error() {
288        let config_err = ConfigError::NoHomeDir;
289        let mi6_err: Mi6Error = config_err.into();
290        assert!(matches!(mi6_err, Mi6Error::Config(_)));
291    }
292
293    #[test]
294    fn test_mi6_error_from_ttl_parse_error() {
295        let ttl_err = TtlParseError::InvalidFormat("bad".to_string());
296        let mi6_err: Mi6Error = ttl_err.into();
297        assert!(matches!(mi6_err, Mi6Error::TtlParse(_)));
298    }
299
300    #[test]
301    fn test_mi6_error_from_init_error() {
302        let init_err = InitError::UnknownFramework("test".to_string());
303        let mi6_err: Mi6Error = init_err.into();
304        assert!(matches!(mi6_err, Mi6Error::Init(_)));
305    }
306
307    #[test]
308    fn test_mi6_error_from_transcript_error() {
309        let transcript_err = TranscriptError::NotFound("file.json".to_string());
310        let mi6_err: Mi6Error = transcript_err.into();
311        assert!(matches!(mi6_err, Mi6Error::Transcript(_)));
312    }
313
314    #[test]
315    fn test_mi6_error_from_scan_error() {
316        let scan_err = ScanError::Parse(TranscriptError::NotFound("file.json".to_string()));
317        let mi6_err: Mi6Error = scan_err.into();
318        assert!(matches!(mi6_err, Mi6Error::Scan(_)));
319    }
320
321    #[test]
322    fn test_mi6_error_display_transparent() {
323        let storage_err = StorageError::Query(Box::new(StringError("query failed".to_string())));
324        let mi6_err: Mi6Error = storage_err.into();
325        // #[error(transparent)] means it displays the inner error's message
326        assert!(mi6_err.to_string().contains("query failed"));
327    }
328
329    #[test]
330    fn test_storage_error_source_preserved() {
331        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
332        let storage_err = StorageError::Connection(Box::new(io_err));
333
334        use std::error::Error;
335        let source = storage_err.source();
336        assert!(source.is_some());
337        assert!(source.is_some_and(|s| s.to_string().contains("file not found")));
338    }
339}