Skip to main content

ddd_toolkit_core/port/
error.rs

1use std::fmt::Display;
2
3/// The general category of a [`PortError`], so callers can react (e.g.
4/// retry) without inspecting the underlying error.
5///
6/// # Examples
7///
8/// See [`PortError`](PortError#examples) for a worked example of matching
9/// on a `PortErrorKind`.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum PortErrorKind {
13    /// The operation conflicted with concurrent state (e.g. a stale version
14    /// on `Save`).
15    Conflict,
16    /// The port is temporarily unreachable (e.g. a network/connection
17    /// failure); retrying later may succeed.
18    Unavailable,
19    /// Any failure not covered by a more specific kind.
20    Other,
21}
22
23/// An error from a port implementation (repository, event dispatcher,
24/// etc.), tagged with a [`PortErrorKind`] and wrapping the underlying cause.
25///
26/// # Examples
27///
28/// ```
29/// use ddd_toolkit_core::port::{PortError, PortErrorKind};
30///
31/// let error = PortError::unavailable("connection refused");
32///
33/// // the kind lets a caller decide how to react, e.g. retry on `Unavailable`...
34/// match error.kind() {
35///     PortErrorKind::Unavailable => { /* retry later */ }
36///     PortErrorKind::Conflict => { /* reload and retry */ }
37///     _ => { /* give up */ }
38/// }
39///
40/// // ...while std::error::Error::source() still exposes the underlying cause.
41/// assert_eq!(error.to_string(), "Unavailable: connection refused");
42/// ```
43#[derive(Debug)]
44pub struct PortError {
45    kind: PortErrorKind,
46    source: Box<dyn std::error::Error + Send + Sync>,
47}
48
49impl PortError {
50    /// Creates a new `PortError` of the given kind, wrapping `source`.
51    pub fn new(
52        kind: PortErrorKind,
53        source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
54    ) -> Self {
55        Self {
56            kind,
57            source: source.into(),
58        }
59    }
60
61    /// Creates a [`PortErrorKind::Conflict`] error.
62    pub fn conflict(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
63        Self::new(PortErrorKind::Conflict, source)
64    }
65
66    /// Creates a [`PortErrorKind::Unavailable`] error.
67    pub fn unavailable(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
68        Self::new(PortErrorKind::Unavailable, source)
69    }
70
71    /// Creates a [`PortErrorKind::Other`] error.
72    pub fn other(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
73        Self::new(PortErrorKind::Other, source)
74    }
75
76    /// This error's kind.
77    pub fn kind(&self) -> PortErrorKind {
78        self.kind
79    }
80}
81
82impl Display for PortError {
83    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
84        write!(f, "{:?}: {}", self.kind, self.source)
85    }
86}
87
88impl std::error::Error for PortError {
89    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
90        Some(self.source.as_ref())
91    }
92}
93
94#[cfg(test)]
95mod test {
96    use super::*;
97
98    #[test]
99    fn new_returns_error_with_given_kind() {
100        let error = PortError::new(PortErrorKind::Conflict, "boom");
101
102        assert_eq!(error.kind(), PortErrorKind::Conflict);
103    }
104
105    #[test]
106    fn conflict_returns_error_with_conflict_kind() {
107        let error = PortError::conflict("boom");
108
109        assert_eq!(error.kind(), PortErrorKind::Conflict);
110    }
111
112    #[test]
113    fn unavailable_returns_error_with_unavailable_kind() {
114        let error = PortError::unavailable("boom");
115
116        assert_eq!(error.kind(), PortErrorKind::Unavailable);
117    }
118
119    #[test]
120    fn other_returns_error_with_other_kind() {
121        let error = PortError::other("boom");
122
123        assert_eq!(error.kind(), PortErrorKind::Other);
124    }
125
126    #[test]
127    fn source_returns_inner_error() {
128        let error: &dyn std::error::Error = &PortError::conflict("boom");
129
130        let source = error.source().expect("source should be Some");
131        assert_eq!(source.to_string(), "boom");
132    }
133
134    #[derive(Debug)]
135    struct MiddleError {
136        inner: Box<dyn std::error::Error + Send + Sync>,
137    }
138
139    impl Display for MiddleError {
140        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
141            write!(f, "middle")
142        }
143    }
144
145    impl std::error::Error for MiddleError {
146        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
147            Some(self.inner.as_ref())
148        }
149    }
150
151    #[test]
152    fn source_preserves_nested_error_chain() {
153        let middle = MiddleError {
154            inner: "root".into(),
155        };
156        let error: &dyn std::error::Error = &PortError::other(middle);
157
158        let middle = error.source().expect("source should be Some");
159        let root = middle.source().expect("nested source should be Some");
160        assert_eq!(middle.to_string(), "middle");
161        assert_eq!(root.to_string(), "root");
162    }
163
164    #[test]
165    fn display_formats_kind_and_source_message() {
166        let error = PortError::conflict("boom");
167
168        assert_eq!(error.to_string(), "Conflict: boom");
169    }
170
171    #[test]
172    fn new_accepts_str_string_and_custom_error() {
173        let _ = PortError::new(PortErrorKind::Other, "str source");
174        let _ = PortError::new(PortErrorKind::Other, String::from("string source"));
175        let _ = PortError::new(
176            PortErrorKind::Other,
177            MiddleError {
178                inner: "root".into(),
179            },
180        );
181    }
182
183    #[test]
184    fn kind_copy_produces_equal_value() {
185        let kind = PortErrorKind::Unavailable;
186        let copied = kind;
187
188        assert_eq!(copied, kind);
189    }
190
191    #[test]
192    fn kind_eq_returns_true_for_same_variant() {
193        assert_eq!(PortErrorKind::Conflict, PortErrorKind::Conflict);
194    }
195
196    #[test]
197    fn kind_eq_returns_false_for_different_variant() {
198        assert_ne!(PortErrorKind::Conflict, PortErrorKind::Unavailable);
199    }
200
201    #[test]
202    fn display_with_empty_source_message_ends_with_colon_space() {
203        let error = PortError::other("");
204
205        assert_eq!(error.to_string(), "Other: ");
206    }
207}