Skip to main content

hexeract_core/
error.rs

1use std::time::Duration;
2use thiserror::Error;
3
4/// Top-level error type for the Hexeract framework.
5///
6/// This enum is marked `#[non_exhaustive]` so that new variants can be added
7/// in minor versions without breaking downstream `match` arms.
8#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum HexeractError {
11    /// No handler was registered for the given message type.
12    #[error("no handler registered for `{message_type}`")]
13    HandlerNotFound {
14        /// The fully-qualified type name of the unregistered command, query or
15        /// notification.
16        message_type: &'static str,
17    },
18
19    /// A handler returned an error. The original error is preserved as source.
20    #[error("handler failed: {source}")]
21    HandlerFailed {
22        /// The original error returned by the handler.
23        #[source]
24        source: Box<dyn std::error::Error + Send + Sync>,
25    },
26
27    /// A dispatch exceeded its configured deadline.
28    #[error("dispatch of `{type_name}` timed out after {duration:?}")]
29    #[non_exhaustive]
30    Timeout {
31        /// Fully-qualified type name of the message being dispatched.
32        type_name: &'static str,
33        /// Configured timeout that was exceeded.
34        duration: Duration,
35    },
36
37    /// A dispatch produced a value that could not be downcast to the expected
38    /// output type.
39    ///
40    /// This indicates a short-circuiting [`Middleware`](crate::middleware::Middleware)
41    /// boxed a value whose type is not the message's `Output`. A correct
42    /// short-circuit must box exactly the dispatched message's output type.
43    #[error("dispatch produced a value that is not the expected output type `{expected}`")]
44    #[non_exhaustive]
45    DowncastFailed {
46        /// Fully-qualified name of the output type the dispatch expected.
47        expected: &'static str,
48    },
49
50    /// A dispatch was cancelled before the handler produced a result, for
51    /// example because its [`HandlerContext`](crate::HandlerContext)
52    /// cancellation token fired.
53    #[error("dispatch of `{type_name}` was cancelled")]
54    #[non_exhaustive]
55    Cancelled {
56        /// Fully-qualified type name of the message whose dispatch was cancelled.
57        type_name: &'static str,
58    },
59
60    /// A generic dispatch-level error with a human-readable message.
61    ///
62    /// Reserved as a last resort for cases that have no dedicated structured
63    /// variant, such as aggregating several notification handler failures into
64    /// one message, or reporting a framework invariant violation. Prefer a
65    /// specific variant ([`HandlerNotFound`](Self::HandlerNotFound),
66    /// [`DowncastFailed`](Self::DowncastFailed), [`Cancelled`](Self::Cancelled),
67    /// ...) whenever one applies.
68    #[error("dispatch error: {0}")]
69    Dispatch(String),
70}
71
72impl HexeractError {
73    /// Wraps any `Send + Sync` error as a [`HexeractError::HandlerFailed`].
74    pub fn handler_failed(source: impl std::error::Error + Send + Sync + 'static) -> Self {
75        Self::HandlerFailed {
76            source: Box::new(source),
77        }
78    }
79
80    /// Builds a [`HexeractError::Timeout`] from the dispatched message type
81    /// name and the timeout that was exceeded. This is the only way to
82    /// construct the variant from outside this crate, since it is marked
83    /// `#[non_exhaustive]`.
84    #[must_use]
85    pub fn timeout(type_name: &'static str, duration: Duration) -> Self {
86        Self::Timeout {
87            type_name,
88            duration,
89        }
90    }
91
92    /// Builds a [`HexeractError::DowncastFailed`] from the fully-qualified name
93    /// of the output type the dispatch expected. This is the only way to
94    /// construct the variant from outside this crate, since it is marked
95    /// `#[non_exhaustive]`.
96    #[must_use]
97    pub fn downcast_failed(expected: &'static str) -> Self {
98        Self::DowncastFailed { expected }
99    }
100
101    /// Builds a [`HexeractError::Cancelled`] from the fully-qualified name of
102    /// the message whose dispatch was cancelled. This is the only way to
103    /// construct the variant from outside this crate, since it is marked
104    /// `#[non_exhaustive]`.
105    #[must_use]
106    pub fn cancelled(type_name: &'static str) -> Self {
107        Self::Cancelled { type_name }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn handler_not_found_display() {
117        let err = HexeractError::HandlerNotFound {
118            message_type: "RegisterUser",
119        };
120        assert_eq!(err.to_string(), "no handler registered for `RegisterUser`");
121    }
122
123    #[test]
124    fn cancelled_names_the_message_type() {
125        let err = HexeractError::cancelled("my::RegisterUser");
126        let rendered = err.to_string();
127        assert!(rendered.contains("RegisterUser"));
128        assert!(rendered.contains("cancelled"));
129        assert!(
130            matches!(err, HexeractError::Cancelled { type_name } if type_name == "my::RegisterUser")
131        );
132    }
133
134    #[test]
135    fn timeout_display_shows_type_name_and_duration() {
136        let err = HexeractError::Timeout {
137            type_name: "my::RegisterUser",
138            duration: Duration::from_secs(5),
139        };
140        let rendered = err.to_string();
141        assert!(rendered.contains("RegisterUser"));
142        assert!(rendered.contains("5s"));
143    }
144
145    #[test]
146    fn handler_failed_preserves_source() {
147        let original = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
148        let err = HexeractError::handler_failed(original);
149        assert!(err.to_string().contains("handler failed"));
150        assert!(std::error::Error::source(&err).is_some());
151    }
152
153    #[test]
154    fn downcast_failed_names_the_expected_output_type() {
155        let err = HexeractError::downcast_failed("u32");
156        let rendered = err.to_string();
157        assert!(rendered.contains("u32"));
158        assert!(matches!(err, HexeractError::DowncastFailed { expected } if expected == "u32"));
159    }
160}