hojicha_core/
error.rs

1//! Error handling for the hojicha framework
2//!
3//! This module provides a structured approach to error handling throughout the framework.
4
5use std::fmt;
6use std::io;
7
8/// Result type alias for hojicha operations
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// Main error type for the hojicha framework
12#[derive(Debug)]
13pub enum Error {
14    /// I/O error (terminal operations, file access, etc.)
15    Io(io::Error),
16
17    /// Terminal-specific error
18    Terminal(String),
19
20    /// Event handling error
21    Event(String),
22
23    /// Command execution error
24    Command(String),
25
26    /// Component error
27    Component(String),
28
29    /// Model update error
30    Model(String),
31
32    /// Configuration error
33    Config(String),
34
35    /// Custom error for user-defined errors
36    Custom(Box<dyn std::error::Error + Send + Sync>),
37}
38
39impl fmt::Display for Error {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Error::Io(err) => write!(f, "I/O error: {err}"),
43            Error::Terminal(msg) => write!(f, "Terminal error: {msg}"),
44            Error::Event(msg) => write!(f, "Event error: {msg}"),
45            Error::Command(msg) => write!(f, "Command error: {msg}"),
46            Error::Component(msg) => write!(f, "Component error: {msg}"),
47            Error::Model(msg) => write!(f, "Model error: {msg}"),
48            Error::Config(msg) => write!(f, "Configuration error: {msg}"),
49            Error::Custom(err) => write!(f, "Custom error: {err}"),
50        }
51    }
52}
53
54impl std::error::Error for Error {
55    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
56        match self {
57            Error::Io(err) => Some(err),
58            Error::Custom(err) => Some(err.as_ref()),
59            _ => None,
60        }
61    }
62}
63
64impl From<io::Error> for Error {
65    fn from(err: io::Error) -> Self {
66        Error::Io(err)
67    }
68}
69
70impl From<std::sync::mpsc::RecvError> for Error {
71    fn from(err: std::sync::mpsc::RecvError) -> Self {
72        Error::Event(format!("Channel receive error: {err}"))
73    }
74}
75
76impl<T> From<std::sync::mpsc::SendError<T>> for Error {
77    fn from(err: std::sync::mpsc::SendError<T>) -> Self {
78        Error::Event(format!("Channel send error: {err}"))
79    }
80}
81
82/// Error context trait for adding context to errors
83pub trait ErrorContext<T> {
84    /// Add context to an error
85    fn context(self, msg: &str) -> Result<T>;
86
87    /// Add context with a closure
88    fn with_context<F>(self, f: F) -> Result<T>
89    where
90        F: FnOnce() -> String;
91}
92
93impl<T, E> ErrorContext<T> for std::result::Result<T, E>
94where
95    E: Into<Error>,
96{
97    fn context(self, msg: &str) -> Result<T> {
98        self.map_err(|err| {
99            let base_error = err.into();
100            Error::Custom(Box::new(ContextError {
101                context: msg.to_string(),
102                source: base_error,
103            }))
104        })
105    }
106
107    fn with_context<F>(self, f: F) -> Result<T>
108    where
109        F: FnOnce() -> String,
110    {
111        self.map_err(|err| {
112            let base_error = err.into();
113            Error::Custom(Box::new(ContextError {
114                context: f(),
115                source: base_error,
116            }))
117        })
118    }
119}
120
121/// Error with additional context
122#[derive(Debug)]
123struct ContextError {
124    context: String,
125    source: Error,
126}
127
128impl fmt::Display for ContextError {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "{}: {}", self.context, self.source)
131    }
132}
133
134impl std::error::Error for ContextError {
135    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
136        Some(&self.source)
137    }
138}
139
140/// Error handler trait for models
141pub trait ErrorHandler {
142    /// Handle an error, returning true if the error was handled
143    fn handle_error(&mut self, error: Error) -> bool;
144}
145
146/// Default error handler that logs errors to stderr
147pub struct DefaultErrorHandler;
148
149impl ErrorHandler for DefaultErrorHandler {
150    fn handle_error(&mut self, error: Error) -> bool {
151        eprintln!("Error: {error}");
152
153        // Print error chain
154        let mut current_error: &dyn std::error::Error = &error;
155        while let Some(source) = current_error.source() {
156            eprintln!("  Caused by: {source}");
157            current_error = source;
158        }
159
160        false // Error not handled, program should exit
161    }
162}
163
164/// Panic handler for converting panics to errors
165pub fn set_panic_handler() {
166    std::panic::set_hook(Box::new(|panic_info| {
167        let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
168            s.to_string()
169        } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
170            s.clone()
171        } else {
172            "Unknown panic".to_string()
173        };
174
175        let location = if let Some(location) = panic_info.location() {
176            format!(
177                " at {}:{}:{}",
178                location.file(),
179                location.line(),
180                location.column()
181            )
182        } else {
183            String::new()
184        };
185
186        eprintln!("Panic occurred: {msg}{location}");
187    }));
188}
189
190/// Helper macro for creating errors with context
191#[macro_export]
192macro_rules! bail {
193    ($msg:literal $(,)?) => {
194        return Err($crate::error::Error::Custom(
195            format!($msg).into()
196        ))
197    };
198    ($err:expr $(,)?) => {
199        return Err($crate::error::Error::Custom(
200            format!("{}", $err).into()
201        ))
202    };
203    ($fmt:expr, $($arg:tt)*) => {
204        return Err($crate::error::Error::Custom(
205            format!($fmt, $($arg)*).into()
206        ))
207    };
208}
209
210/// Helper macro for ensuring conditions
211#[macro_export]
212macro_rules! ensure {
213    ($cond:expr, $msg:literal $(,)?) => {
214        if !$cond {
215            $crate::bail!($msg);
216        }
217    };
218    ($cond:expr, $err:expr $(,)?) => {
219        if !$cond {
220            $crate::bail!($err);
221        }
222    };
223    ($cond:expr, $fmt:expr, $($arg:tt)*) => {
224        if !$cond {
225            $crate::bail!($fmt, $($arg)*);
226        }
227    };
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use std::error::Error as StdError;
234
235    #[test]
236    fn test_error_display() {
237        let err = Error::Terminal("Failed to initialize".to_string());
238        assert_eq!(err.to_string(), "Terminal error: Failed to initialize");
239
240        let err = Error::Io(io::Error::new(io::ErrorKind::NotFound, "File not found"));
241        assert_eq!(err.to_string(), "I/O error: File not found");
242    }
243
244    #[test]
245    fn test_error_context() {
246        let result: Result<()> = Err(Error::Terminal("Base error".to_string()));
247        let with_context = result.context("While initializing terminal");
248
249        assert!(with_context.is_err());
250        let err_str = with_context.unwrap_err().to_string();
251        assert!(err_str.contains("While initializing terminal"));
252        assert!(err_str.contains("Base error"));
253    }
254
255    #[test]
256    fn test_error_from_io() {
257        let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "Access denied");
258        let err: Error = io_err.into();
259
260        match err {
261            Error::Io(_) => (),
262            _ => panic!("Expected Io error variant"),
263        }
264    }
265
266    #[test]
267    fn test_bail_macro() {
268        fn test_fn() -> Result<()> {
269            bail!("Test error");
270        }
271
272        assert!(test_fn().is_err());
273        assert_eq!(
274            test_fn().unwrap_err().to_string(),
275            "Custom error: Test error"
276        );
277    }
278
279    #[test]
280    fn test_ensure_macro() {
281        fn test_fn(value: i32) -> Result<i32> {
282            ensure!(value > 0, "Value must be positive");
283            Ok(value)
284        }
285
286        assert!(test_fn(5).is_ok());
287        assert!(test_fn(-1).is_err());
288    }
289
290    #[test]
291    fn test_all_error_variants() {
292        let errors = vec![
293            Error::Terminal("terminal error".to_string()),
294            Error::Event("event error".to_string()),
295            Error::Command("command error".to_string()),
296            Error::Component("component error".to_string()),
297            Error::Model("model error".to_string()),
298            Error::Config("config error".to_string()),
299        ];
300
301        for error in errors {
302            let display_str = error.to_string();
303            assert!(!display_str.is_empty());
304
305            // Test that source returns None for string-based errors
306            assert!(error.source().is_none());
307        }
308    }
309
310    #[test]
311    fn test_error_source_chain() {
312        let io_err = io::Error::new(io::ErrorKind::NotFound, "File not found");
313        let err = Error::Io(io_err);
314
315        // Should have a source
316        assert!(StdError::source(&err).is_some());
317
318        let source = err.source().unwrap();
319        assert_eq!(source.to_string(), "File not found");
320    }
321
322    #[test]
323    fn test_custom_error() {
324        let custom_err = Box::new(io::Error::other("custom"));
325        let err = Error::Custom(custom_err);
326
327        assert!(StdError::source(&err).is_some());
328        assert!(err.to_string().contains("Custom error"));
329    }
330
331    #[test]
332    fn test_channel_error_conversions() {
333        let recv_err = std::sync::mpsc::RecvError;
334        let err: Error = recv_err.into();
335
336        match err {
337            Error::Event(_) => (),
338            _ => panic!("Expected Event error variant"),
339        }
340
341        let (tx, _rx) = std::sync::mpsc::channel::<i32>();
342        drop(_rx); // Close receiver to cause send error
343
344        let send_result = tx.send(42);
345        if let Err(send_err) = send_result {
346            let err: Error = send_err.into();
347            match err {
348                Error::Event(_) => (),
349                _ => panic!("Expected Event error variant"),
350            }
351        }
352    }
353
354    #[test]
355    fn test_with_context() {
356        let result: Result<()> = Err(Error::Terminal("Base error".to_string()));
357        let with_context = result.with_context(|| "Dynamic context".to_string());
358
359        assert!(with_context.is_err());
360        let err_str = with_context.unwrap_err().to_string();
361        assert!(err_str.contains("Dynamic context"));
362    }
363
364    #[test]
365    fn test_default_error_handler() {
366        let mut handler = DefaultErrorHandler;
367        let error = Error::Terminal("test error".to_string());
368
369        // Should return false (error not handled)
370        assert!(!handler.handle_error(error));
371    }
372
373    #[test]
374    fn test_context_error() {
375        let base_error = Error::Terminal("base".to_string());
376        let context_error = ContextError {
377            context: "context".to_string(),
378            source: base_error,
379        };
380
381        let display_str = context_error.to_string();
382        assert!(display_str.contains("context"));
383        assert!(display_str.contains("base"));
384
385        assert!(StdError::source(&context_error).is_some());
386    }
387
388    #[test]
389    fn test_bail_macro_with_format() {
390        fn test_fn(value: i32) -> Result<()> {
391            bail!("Value {} is invalid", value);
392        }
393
394        let err = test_fn(42).unwrap_err();
395        assert!(err.to_string().contains("Value 42 is invalid"));
396    }
397
398    #[test]
399    fn test_ensure_macro_with_format() {
400        fn test_fn(value: i32, min: i32) -> Result<i32> {
401            ensure!(value >= min, "Value {} must be >= {}", value, min);
402            Ok(value)
403        }
404
405        assert!(test_fn(10, 5).is_ok());
406
407        let err = test_fn(3, 5).unwrap_err();
408        assert!(err.to_string().contains("Value 3 must be >= 5"));
409    }
410
411    #[test]
412    fn test_panic_handler() {
413        // Test that we can set the panic handler without panicking
414        set_panic_handler();
415
416        // Reset to default handler
417        let _ = std::panic::take_hook();
418    }
419}