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 command or query type.
12    #[error("no handler registered for `{command_type}`")]
13    HandlerNotFound {
14        /// The fully-qualified type name of the unregistered command or query.
15        command_type: &'static str,
16    },
17
18    /// A handler returned an error. The original error is preserved as source.
19    #[error("handler failed: {source}")]
20    HandlerFailed {
21        /// The original error returned by the handler.
22        #[source]
23        source: Box<dyn std::error::Error + Send + Sync>,
24    },
25
26    /// A dispatch exceeded its configured deadline.
27    #[error("dispatch of `{type_name}` timed out after {duration:?}")]
28    #[non_exhaustive]
29    Timeout {
30        /// Fully-qualified type name of the message being dispatched.
31        type_name: &'static str,
32        /// Configured timeout that was exceeded.
33        duration: Duration,
34    },
35
36    /// A dispatch produced a value that could not be downcast to the expected
37    /// output type.
38    ///
39    /// This indicates a short-circuiting [`Middleware`](crate::middleware::Middleware)
40    /// boxed a value whose type is not the message's `Output`. A correct
41    /// short-circuit must box exactly the dispatched message's output type.
42    #[error("dispatch produced a value that is not the expected output type `{expected}`")]
43    #[non_exhaustive]
44    DowncastFailed {
45        /// Fully-qualified name of the output type the dispatch expected.
46        expected: &'static str,
47    },
48
49    /// A generic dispatch-level error with a human-readable message.
50    #[error("dispatch error: {0}")]
51    Dispatch(String),
52}
53
54impl HexeractError {
55    /// Wraps any `Send + Sync` error as a [`HexeractError::HandlerFailed`].
56    pub fn handler_failed(source: impl std::error::Error + Send + Sync + 'static) -> Self {
57        Self::HandlerFailed {
58            source: Box::new(source),
59        }
60    }
61
62    /// Builds a [`HexeractError::Timeout`] from the dispatched message type
63    /// name and the timeout that was exceeded. This is the only way to
64    /// construct the variant from outside this crate, since it is marked
65    /// `#[non_exhaustive]`.
66    #[must_use]
67    pub fn timeout(type_name: &'static str, duration: Duration) -> Self {
68        Self::Timeout {
69            type_name,
70            duration,
71        }
72    }
73
74    /// Builds a [`HexeractError::DowncastFailed`] from the fully-qualified name
75    /// of the output type the dispatch expected. This is the only way to
76    /// construct the variant from outside this crate, since it is marked
77    /// `#[non_exhaustive]`.
78    #[must_use]
79    pub fn downcast_failed(expected: &'static str) -> Self {
80        Self::DowncastFailed { expected }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn handler_not_found_display() {
90        let err = HexeractError::HandlerNotFound {
91            command_type: "RegisterUser",
92        };
93        assert_eq!(err.to_string(), "no handler registered for `RegisterUser`");
94    }
95
96    #[test]
97    fn timeout_display_shows_type_name_and_duration() {
98        let err = HexeractError::Timeout {
99            type_name: "my::RegisterUser",
100            duration: Duration::from_secs(5),
101        };
102        let rendered = err.to_string();
103        assert!(rendered.contains("RegisterUser"));
104        assert!(rendered.contains("5s"));
105    }
106
107    #[test]
108    fn handler_failed_preserves_source() {
109        let original = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
110        let err = HexeractError::handler_failed(original);
111        assert!(err.to_string().contains("handler failed"));
112        assert!(std::error::Error::source(&err).is_some());
113    }
114
115    #[test]
116    fn downcast_failed_names_the_expected_output_type() {
117        let err = HexeractError::downcast_failed("u32");
118        let rendered = err.to_string();
119        assert!(rendered.contains("u32"));
120        assert!(matches!(err, HexeractError::DowncastFailed { expected } if expected == "u32"));
121    }
122}