mod_events/error.rs
1//! Error types exposed by the dispatcher.
2//!
3//! The crate distinguishes two failure surfaces:
4//!
5//! - [`ListenerError`] wraps the error value returned by a user-supplied
6//! listener. Listeners can construct one from any `Error + Send + Sync +
7//! 'static`, from a `&str`, from a `String`, or from a pre-existing
8//! `Box<dyn Error + Send + Sync>`. The dispatcher itself never inspects
9//! the inner error — it only stores and exposes it through
10//! [`crate::DispatchResult`].
11//!
12//! There is intentionally no top-level `DispatchError` enum yet: every
13//! dispatcher method is currently infallible because the underlying lock
14//! primitive (`parking_lot::RwLock`) does not poison. A new enum will be
15//! added the first time a dispatcher operation grows a real failure mode.
16
17use std::error::Error;
18use std::fmt;
19
20/// Opaque error returned by an event listener.
21///
22/// `ListenerError` is the typed wrapper the dispatcher stores in
23/// [`crate::DispatchResult`] for every failing listener. It hides the
24/// concrete error type a listener returned, while still letting callers
25/// inspect the chain via [`std::error::Error::source`].
26///
27/// # Constructing
28///
29/// ```rust
30/// use mod_events::ListenerError;
31/// use std::io;
32///
33/// // From any Error + Send + Sync + 'static
34/// let from_err = ListenerError::new(io::Error::new(io::ErrorKind::Other, "bad"));
35///
36/// // From a string literal or owned String, via Into
37/// let from_str: ListenerError = "validation failed".into();
38/// let from_string: ListenerError = format!("retry budget exhausted").into();
39/// ```
40#[derive(Debug)]
41pub struct ListenerError(Box<dyn Error + Send + Sync + 'static>);
42
43impl ListenerError {
44 /// Wrap any error value that implements `Error + Send + Sync + 'static`.
45 pub fn new<E>(error: E) -> Self
46 where
47 E: Error + Send + Sync + 'static,
48 {
49 Self(Box::new(error))
50 }
51
52 /// Wrap a textual message as a listener error.
53 pub fn message<S: Into<String>>(msg: S) -> Self {
54 Self(msg.into().into())
55 }
56
57 /// Borrow the underlying error trait object.
58 #[must_use]
59 pub fn inner(&self) -> &(dyn Error + Send + Sync + 'static) {
60 self.0.as_ref()
61 }
62
63 /// Consume the wrapper and return the inner boxed error.
64 #[must_use]
65 pub fn into_inner(self) -> Box<dyn Error + Send + Sync + 'static> {
66 self.0
67 }
68}
69
70impl fmt::Display for ListenerError {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 fmt::Display::fmt(&self.0, f)
73 }
74}
75
76impl Error for ListenerError {
77 fn source(&self) -> Option<&(dyn Error + 'static)> {
78 Some(self.0.as_ref())
79 }
80}
81
82impl From<Box<dyn Error + Send + Sync + 'static>> for ListenerError {
83 fn from(boxed: Box<dyn Error + Send + Sync + 'static>) -> Self {
84 Self(boxed)
85 }
86}
87
88impl From<&str> for ListenerError {
89 fn from(s: &str) -> Self {
90 Self::message(s)
91 }
92}
93
94impl From<String> for ListenerError {
95 fn from(s: String) -> Self {
96 Self::message(s)
97 }
98}
99
100/// Convert a panic payload (the value carried out of a unwinding
101/// listener) into a [`ListenerError`].
102///
103/// `std::panic::catch_unwind` returns the panic payload as
104/// `Box<dyn Any + Send>`. The dispatcher uses this helper to turn that
105/// payload into the same `ListenerError` shape a well-behaved listener
106/// would have returned, so `DispatchResult::errors()` is the single
107/// place the caller has to look for failures — panics included.
108///
109/// Most panics carry a `&'static str` (from `panic!("…")`) or a
110/// `String` (from `panic!("{}", x)`); both are extracted directly.
111/// Anything else degrades to a generic `"listener panicked"` message.
112pub(crate) fn panic_payload_to_listener_error(
113 payload: Box<dyn std::any::Any + Send>,
114) -> ListenerError {
115 let detail = if let Some(s) = payload.downcast_ref::<&'static str>() {
116 (*s).to_owned()
117 } else if let Some(s) = payload.downcast_ref::<String>() {
118 s.clone()
119 } else {
120 String::from("<non-string panic payload>")
121 };
122 ListenerError::message(format!("listener panicked: {detail}"))
123}