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