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