Skip to main content

ddd_toolkit_core/port/
event.rs

1use std::fmt;
2
3use crate::{domain::DomainEvent, port::PortError};
4
5/// A `dispatch` failure that reports which events were not delivered, so a
6/// caller can retry or persist just the remainder instead of losing track
7/// of the whole batch.
8///
9/// Implementors that dispatch events one at a time should populate
10/// `undelivered` with every event from the input that was not confirmed
11/// delivered (typically the failing event and everything after it).
12/// Implementors that dispatch atomically (all-or-nothing) should populate
13/// it with the entire input batch on failure.
14///
15/// # Examples
16///
17/// See [`EventDispatcher`](EventDispatcher#examples) for a worked example
18/// of handling a `DispatchError`.
19#[derive(Debug)]
20pub struct DispatchError<E> {
21    /// Events from the input batch that were not confirmed delivered.
22    pub undelivered: Vec<E>,
23    /// The underlying failure.
24    pub source: PortError,
25}
26
27impl<E> DispatchError<E> {
28    /// Creates a new `DispatchError`.
29    pub fn new(undelivered: Vec<E>, source: PortError) -> Self {
30        Self {
31            undelivered,
32            source,
33        }
34    }
35}
36
37impl<E: fmt::Debug> fmt::Display for DispatchError<E> {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(
40            f,
41            "dispatch failed with {} undelivered event(s): {}",
42            self.undelivered.len(),
43            self.source
44        )
45    }
46}
47
48impl<E: fmt::Debug> std::error::Error for DispatchError<E> {
49    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
50        Some(&self.source)
51    }
52}
53
54/// Publishes recorded [`DomainEvent`]s, e.g. to a message broker.
55///
56/// # Examples
57///
58/// ```
59/// use ddd_toolkit_core::domain::DomainEvent;
60/// use ddd_toolkit_core::port::PortError;
61/// use ddd_toolkit_core::port::event::{DispatchError, EventDispatcher};
62///
63/// # fn block_on<F: std::future::Future>(future: F) -> F::Output {
64/// #     let mut future = std::pin::pin!(future);
65/// #     let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
66/// #     loop {
67/// #         if let std::task::Poll::Ready(output) = future.as_mut().poll(&mut cx) {
68/// #             return output;
69/// #         }
70/// #     }
71/// # }
72/// #
73/// #[derive(Debug)]
74/// struct OrderPlaced;
75///
76/// impl DomainEvent for OrderPlaced {}
77///
78/// struct BrokenBroker;
79///
80/// impl EventDispatcher<OrderPlaced> for BrokenBroker {
81///     async fn dispatch(
82///         &self,
83///         events: Vec<OrderPlaced>,
84///     ) -> Result<(), DispatchError<OrderPlaced>> {
85///         Err(DispatchError::new(events, PortError::unavailable("broker down")))
86///     }
87/// }
88///
89/// let error = block_on(BrokenBroker.dispatch(vec![OrderPlaced]))
90///     .expect_err("broker is down, dispatch should fail");
91///
92/// // the events that weren't delivered are handed back, so the caller can
93/// // decide whether to retry them
94/// assert_eq!(error.undelivered.len(), 1);
95/// ```
96#[trait_variant::make(Send)]
97pub trait EventDispatcher<E: DomainEvent> {
98    /// Dispatches `events`. See [`DispatchError`] for how partial failure
99    /// is reported.
100    async fn dispatch(&self, events: Vec<E>) -> Result<(), DispatchError<E>>;
101}
102
103#[cfg(test)]
104mod test {
105    use std::sync::Mutex;
106
107    use crate::{port::PortErrorKind, testing::block_on};
108
109    use super::*;
110
111    #[derive(Debug, Clone, PartialEq)]
112    struct FooEvent {
113        name: String,
114    }
115
116    impl FooEvent {
117        fn new(name: &str) -> Self {
118            Self {
119                name: name.to_string(),
120            }
121        }
122    }
123
124    impl DomainEvent for FooEvent {}
125
126    struct RecordingDispatcher {
127        dispatched: Mutex<Vec<FooEvent>>,
128    }
129
130    impl RecordingDispatcher {
131        fn new() -> Self {
132            Self {
133                dispatched: Mutex::new(Vec::new()),
134            }
135        }
136    }
137
138    impl EventDispatcher<FooEvent> for RecordingDispatcher {
139        async fn dispatch(&self, events: Vec<FooEvent>) -> Result<(), DispatchError<FooEvent>> {
140            self.dispatched.lock().unwrap().extend(events);
141            Ok(())
142        }
143    }
144
145    struct FailingDispatcher;
146
147    impl EventDispatcher<FooEvent> for FailingDispatcher {
148        async fn dispatch(&self, events: Vec<FooEvent>) -> Result<(), DispatchError<FooEvent>> {
149            Err(DispatchError::new(
150                events,
151                PortError::unavailable("dispatch failed"),
152            ))
153        }
154    }
155
156    // Dispatches events one at a time, failing on (and after) the event
157    // whose name is "boom" - a stand-in for a partial-failure dispatcher.
158    struct PartiallyFailingDispatcher;
159
160    impl EventDispatcher<FooEvent> for PartiallyFailingDispatcher {
161        async fn dispatch(&self, events: Vec<FooEvent>) -> Result<(), DispatchError<FooEvent>> {
162            let boom_at = events.iter().position(|e| e.name == "boom");
163            match boom_at {
164                Some(index) => Err(DispatchError::new(
165                    events[index..].to_vec(),
166                    PortError::unavailable("dispatch failed"),
167                )),
168                None => Ok(()),
169            }
170        }
171    }
172
173    #[test]
174    fn dispatch_records_all_events_in_order() {
175        let dispatcher = RecordingDispatcher::new();
176        let events = vec![FooEvent::new("created"), FooEvent::new("renamed")];
177
178        block_on(dispatcher.dispatch(events)).expect("dispatch should succeed");
179
180        assert_eq!(
181            *dispatcher.dispatched.lock().unwrap(),
182            vec![FooEvent::new("created"), FooEvent::new("renamed")]
183        );
184    }
185
186    #[test]
187    fn dispatch_called_twice_delivers_both_batches() {
188        let dispatcher = RecordingDispatcher::new();
189
190        block_on(dispatcher.dispatch(vec![FooEvent::new("created")]))
191            .expect("first dispatch should succeed");
192        block_on(dispatcher.dispatch(vec![FooEvent::new("renamed")]))
193            .expect("second dispatch should succeed");
194
195        assert_eq!(
196            *dispatcher.dispatched.lock().unwrap(),
197            vec![FooEvent::new("created"), FooEvent::new("renamed")]
198        );
199    }
200
201    #[test]
202    fn generic_fn_accepts_event_dispatcher_bound() {
203        fn publish<D: EventDispatcher<FooEvent>>(
204            dispatcher: &D,
205            events: Vec<FooEvent>,
206        ) -> Result<(), DispatchError<FooEvent>> {
207            block_on(dispatcher.dispatch(events))
208        }
209
210        let dispatcher = RecordingDispatcher::new();
211
212        publish(&dispatcher, vec![FooEvent::new("created")]).expect("publish should succeed");
213
214        assert_eq!(
215            *dispatcher.dispatched.lock().unwrap(),
216            vec![FooEvent::new("created")]
217        );
218    }
219
220    #[test]
221    fn dispatch_propagates_port_error() {
222        let dispatcher = FailingDispatcher;
223
224        let error = block_on(dispatcher.dispatch(vec![FooEvent::new("created")]))
225            .expect_err("dispatch should fail");
226
227        assert_eq!(error.source.kind(), PortErrorKind::Unavailable);
228    }
229
230    #[test]
231    fn dispatch_error_reports_the_undelivered_events() {
232        let dispatcher = FailingDispatcher;
233        let events = vec![FooEvent::new("created"), FooEvent::new("renamed")];
234
235        let error = block_on(dispatcher.dispatch(events)).expect_err("dispatch should fail");
236
237        assert_eq!(
238            error.undelivered,
239            vec![FooEvent::new("created"), FooEvent::new("renamed")]
240        );
241    }
242
243    #[test]
244    fn partial_failure_reports_only_the_events_after_the_failure_point() {
245        let dispatcher = PartiallyFailingDispatcher;
246        let events = vec![
247            FooEvent::new("created"),
248            FooEvent::new("boom"),
249            FooEvent::new("renamed"),
250        ];
251
252        let error = block_on(dispatcher.dispatch(events)).expect_err("dispatch should fail");
253
254        assert_eq!(
255            error.undelivered,
256            vec![FooEvent::new("boom"), FooEvent::new("renamed")]
257        );
258    }
259
260    #[test]
261    fn dispatch_error_display_includes_undelivered_count_and_source() {
262        let error = DispatchError::new(
263            vec![FooEvent::new("created")],
264            PortError::unavailable("boom"),
265        );
266
267        assert_eq!(
268            error.to_string(),
269            "dispatch failed with 1 undelivered event(s): Unavailable: boom"
270        );
271    }
272
273    #[test]
274    fn dispatch_empty_vec_is_ok() {
275        let dispatcher = RecordingDispatcher::new();
276
277        block_on(dispatcher.dispatch(Vec::new())).expect("dispatch should succeed");
278
279        assert!(dispatcher.dispatched.lock().unwrap().is_empty());
280    }
281
282    // Compile-time only (no runtime assertion): the generic bound accepts the
283    // future as `Send` purely because `#[trait_variant::make(Send)]` promises
284    // it at the trait level, not because of the concrete impl.
285    #[test]
286    fn dispatcher_future_is_send() {
287        fn assert_send(_: impl Send) {}
288
289        fn check<D: EventDispatcher<FooEvent>>(dispatcher: &D, events: Vec<FooEvent>) {
290            assert_send(dispatcher.dispatch(events));
291        }
292
293        let dispatcher = RecordingDispatcher::new();
294
295        check(&dispatcher, vec![FooEvent::new("created")]);
296    }
297}