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