Skip to main content

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}