1use std::fmt;
6use std::io;
7use thiserror::Error;
8
9pub type Result<T> = std::result::Result<T, Error>;
11
12#[derive(Debug, Error)]
14pub enum Error {
15 #[error("I/O error: {0}")]
17 Io(#[from] io::Error),
18
19 #[error("Terminal error: {0}")]
21 Terminal(String),
22
23 #[error("Event error: {0}")]
25 Event(String),
26
27 #[error("Command error: {0}")]
29 Command(String),
30
31 #[error("Component error: {0}")]
33 Component(String),
34
35 #[error("Model error: {0}")]
37 Model(String),
38
39 #[error("Configuration error: {0}")]
41 Config(String),
42
43 #[error("Custom error: {0}")]
45 Custom(#[source] Box<dyn std::error::Error + Send + Sync>),
46
47 #[error("Async runtime error: {0}")]
49 AsyncRuntime(String),
50
51 #[error("Resource limit exceeded: {0}")]
53 ResourceLimit(String),
54
55 #[error("Validation error: {0}")]
57 Validation(String),
58
59 #[error("Parsing error: {0}")]
61 Parse(String),
62
63 #[error("Operation timed out: {0}")]
65 Timeout(String),
66
67 #[error("Channel send error: {0}")]
69 ChannelSend(String),
70
71 #[error("Channel receive error")]
73 ChannelRecv(#[from] std::sync::mpsc::RecvError),
74}
75
76impl<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
90pub trait ErrorContext<T> {
92 fn context(self, msg: &str) -> Result<T>;
94
95 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#[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
148pub trait ErrorHandler {
150 fn handle_error(&mut self, error: Error) -> bool;
152}
153
154pub struct DefaultErrorHandler;
156
157impl ErrorHandler for DefaultErrorHandler {
158 fn handle_error(&mut self, error: Error) -> bool {
159 eprintln!("Error: {error}");
160
161 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 }
170}
171
172pub 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#[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#[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 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 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); 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 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 set_panic_handler();
423
424 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 assert!(error.source().is_none());
445 }
446 }
447
448 #[test]
449 fn test_timeout_error_conversion() {
450 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 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}