Skip to main content

soaprs_core/
dispatch.rs

1//! Strongly typed dispatch and runtime-independent middleware pipelines.
2
3use std::{fmt, sync::Arc};
4
5use crate::{BoxFuture, Command, CommandHandler, Query, QueryHandler, SoapResult};
6
7/// Dispatches one concrete command type while preserving its output type.
8///
9/// Every [`CommandHandler<C>`] implements this port automatically. It exists
10/// so application boundaries can use dispatch terminology without introducing
11/// a runtime service locator or erasing message and output types.
12pub trait CommandDispatcher<C>: Send + Sync
13where
14    C: Command,
15{
16    /// Dispatches the command to its typed handler or pipeline.
17    fn dispatch(&self, command: C) -> BoxFuture<'_, SoapResult<C::Output>>;
18}
19
20impl<C, H> CommandDispatcher<C> for H
21where
22    C: Command,
23    H: CommandHandler<C> + ?Sized,
24{
25    fn dispatch(&self, command: C) -> BoxFuture<'_, SoapResult<C::Output>> {
26        self.command(command)
27    }
28}
29
30/// Dispatches one concrete query type while preserving its output type.
31///
32/// Every [`QueryHandler<Q>`] implements this port automatically.
33pub trait QueryDispatcher<Q>: Send + Sync
34where
35    Q: Query,
36{
37    /// Dispatches the query to its typed handler or pipeline.
38    fn dispatch(&self, query: Q) -> BoxFuture<'_, SoapResult<Q::Output>>;
39}
40
41impl<Q, H> QueryDispatcher<Q> for H
42where
43    Q: Query,
44    H: QueryHandler<Q> + ?Sized,
45{
46    fn dispatch(&self, query: Q) -> BoxFuture<'_, SoapResult<Q::Output>> {
47        self.query(query)
48    }
49}
50
51/// Remaining command middleware followed by the final typed handler.
52///
53/// Middleware calls [`run`](Self::run) exactly when it wants processing to
54/// continue. It may validate and short-circuit before that call, or inspect and
55/// transform the result after it completes.
56pub struct CommandNext<'a, C>
57where
58    C: Command + 'static,
59{
60    middleware: &'a [Arc<dyn CommandMiddleware<C>>],
61    handler: &'a dyn CommandHandler<C>,
62}
63
64impl<C> fmt::Debug for CommandNext<'_, C>
65where
66    C: Command + 'static,
67{
68    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69        formatter
70            .debug_struct("CommandNext")
71            .field("remaining_middleware", &self.middleware.len())
72            .finish_non_exhaustive()
73    }
74}
75
76impl<'a, C> CommandNext<'a, C>
77where
78    C: Command + 'static,
79{
80    /// Continues the pipeline with the next middleware or final handler.
81    pub fn run(self, command: C) -> BoxFuture<'a, SoapResult<C::Output>> {
82        if let Some((middleware, remaining)) = self.middleware.split_first() {
83            middleware.handle(
84                command,
85                Self {
86                    middleware: remaining,
87                    handler: self.handler,
88                },
89            )
90        } else {
91            self.handler.command(command)
92        }
93    }
94}
95
96/// Intercepts one concrete command type around its next processing step.
97pub trait CommandMiddleware<C>: Send + Sync
98where
99    C: Command + 'static,
100{
101    /// Processes, delegates, or short-circuits the command.
102    fn handle<'a>(
103        &'a self,
104        command: C,
105        next: CommandNext<'a, C>,
106    ) -> BoxFuture<'a, SoapResult<C::Output>>;
107}
108
109/// Ordered middleware pipeline ending in one typed command handler.
110///
111/// Middleware runs in registration order before the handler and unwinds in
112/// reverse order after it. The pipeline also implements [`CommandHandler<C>`],
113/// so existing application services and saga processors can receive it without
114/// depending on a new abstraction.
115pub struct CommandPipeline<C>
116where
117    C: Command + 'static,
118{
119    handler: Arc<dyn CommandHandler<C>>,
120    middleware: Vec<Arc<dyn CommandMiddleware<C>>>,
121}
122
123impl<C> fmt::Debug for CommandPipeline<C>
124where
125    C: Command + 'static,
126{
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        formatter
129            .debug_struct("CommandPipeline")
130            .field("middleware", &self.middleware.len())
131            .finish_non_exhaustive()
132    }
133}
134
135impl<C> CommandPipeline<C>
136where
137    C: Command + 'static,
138{
139    /// Creates an empty pipeline ending in `handler`.
140    pub fn new(handler: Arc<dyn CommandHandler<C>>) -> Self {
141        Self {
142            handler,
143            middleware: Vec::new(),
144        }
145    }
146
147    /// Appends middleware and returns the updated pipeline.
148    #[must_use]
149    pub fn with_middleware(mut self, middleware: Arc<dyn CommandMiddleware<C>>) -> Self {
150        self.middleware.push(middleware);
151        self
152    }
153
154    /// Appends middleware after every previously registered layer.
155    pub fn push_middleware(&mut self, middleware: Arc<dyn CommandMiddleware<C>>) {
156        self.middleware.push(middleware);
157    }
158
159    /// Returns the number of registered middleware layers.
160    pub fn middleware_count(&self) -> usize {
161        self.middleware.len()
162    }
163}
164
165impl<C> CommandHandler<C> for CommandPipeline<C>
166where
167    C: Command + 'static,
168{
169    fn command(&self, command: C) -> BoxFuture<'_, SoapResult<C::Output>> {
170        CommandNext {
171            middleware: &self.middleware,
172            handler: self.handler.as_ref(),
173        }
174        .run(command)
175    }
176}
177
178/// Remaining query middleware followed by the final typed handler.
179pub struct QueryNext<'a, Q>
180where
181    Q: Query + 'static,
182{
183    middleware: &'a [Arc<dyn QueryMiddleware<Q>>],
184    handler: &'a dyn QueryHandler<Q>,
185}
186
187impl<Q> fmt::Debug for QueryNext<'_, Q>
188where
189    Q: Query + 'static,
190{
191    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
192        formatter
193            .debug_struct("QueryNext")
194            .field("remaining_middleware", &self.middleware.len())
195            .finish_non_exhaustive()
196    }
197}
198
199impl<'a, Q> QueryNext<'a, Q>
200where
201    Q: Query + 'static,
202{
203    /// Continues the pipeline with the next middleware or final handler.
204    pub fn run(self, query: Q) -> BoxFuture<'a, SoapResult<Q::Output>> {
205        if let Some((middleware, remaining)) = self.middleware.split_first() {
206            middleware.handle(
207                query,
208                Self {
209                    middleware: remaining,
210                    handler: self.handler,
211                },
212            )
213        } else {
214            self.handler.query(query)
215        }
216    }
217}
218
219/// Intercepts one concrete query type around its next processing step.
220pub trait QueryMiddleware<Q>: Send + Sync
221where
222    Q: Query + 'static,
223{
224    /// Processes, delegates, or short-circuits the query.
225    fn handle<'a>(
226        &'a self,
227        query: Q,
228        next: QueryNext<'a, Q>,
229    ) -> BoxFuture<'a, SoapResult<Q::Output>>;
230}
231
232/// Ordered middleware pipeline ending in one typed query handler.
233pub struct QueryPipeline<Q>
234where
235    Q: Query + 'static,
236{
237    handler: Arc<dyn QueryHandler<Q>>,
238    middleware: Vec<Arc<dyn QueryMiddleware<Q>>>,
239}
240
241impl<Q> fmt::Debug for QueryPipeline<Q>
242where
243    Q: Query + 'static,
244{
245    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246        formatter
247            .debug_struct("QueryPipeline")
248            .field("middleware", &self.middleware.len())
249            .finish_non_exhaustive()
250    }
251}
252
253impl<Q> QueryPipeline<Q>
254where
255    Q: Query + 'static,
256{
257    /// Creates an empty pipeline ending in `handler`.
258    pub fn new(handler: Arc<dyn QueryHandler<Q>>) -> Self {
259        Self {
260            handler,
261            middleware: Vec::new(),
262        }
263    }
264
265    /// Appends middleware and returns the updated pipeline.
266    #[must_use]
267    pub fn with_middleware(mut self, middleware: Arc<dyn QueryMiddleware<Q>>) -> Self {
268        self.middleware.push(middleware);
269        self
270    }
271
272    /// Appends middleware after every previously registered layer.
273    pub fn push_middleware(&mut self, middleware: Arc<dyn QueryMiddleware<Q>>) {
274        self.middleware.push(middleware);
275    }
276
277    /// Returns the number of registered middleware layers.
278    pub fn middleware_count(&self) -> usize {
279        self.middleware.len()
280    }
281}
282
283impl<Q> QueryHandler<Q> for QueryPipeline<Q>
284where
285    Q: Query + 'static,
286{
287    fn query(&self, query: Q) -> BoxFuture<'_, SoapResult<Q::Output>> {
288        QueryNext {
289            middleware: &self.middleware,
290            handler: self.handler.as_ref(),
291        }
292        .run(query)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use std::{
299        future::Future,
300        sync::{Arc, Mutex},
301        task::{Context, Poll, Waker},
302    };
303
304    use crate::{
305        BoxFuture, Command, CommandDispatcher, CommandHandler, CommandMiddleware, CommandNext,
306        CommandPipeline, Query, QueryDispatcher, QueryHandler, QueryMiddleware, QueryNext,
307        QueryPipeline, SoapError, SoapErrorKind, SoapResult,
308    };
309
310    fn block_on<F>(future: F) -> F::Output
311    where
312        F: Future,
313    {
314        let mut context = Context::from_waker(Waker::noop());
315        let mut future = Box::pin(future);
316        loop {
317            match future.as_mut().poll(&mut context) {
318                Poll::Ready(output) => return output,
319                Poll::Pending => std::thread::yield_now(),
320            }
321        }
322    }
323
324    #[derive(Debug)]
325    struct Add(i32);
326
327    impl Command for Add {
328        type Output = i32;
329    }
330
331    struct AddHandler {
332        trace: Arc<Mutex<Vec<&'static str>>>,
333    }
334
335    impl CommandHandler<Add> for AddHandler {
336        fn command(&self, command: Add) -> BoxFuture<'_, SoapResult<i32>> {
337            Box::pin(async move {
338                self.trace
339                    .lock()
340                    .map_err(|_| SoapError::infrastructure("command test trace lock poisoned"))?
341                    .push("handler");
342                Ok(command.0)
343            })
344        }
345    }
346
347    struct AroundCommand {
348        before: &'static str,
349        after: &'static str,
350        add: i32,
351        trace: Arc<Mutex<Vec<&'static str>>>,
352    }
353
354    impl CommandMiddleware<Add> for AroundCommand {
355        fn handle<'a>(
356            &'a self,
357            command: Add,
358            next: CommandNext<'a, Add>,
359        ) -> BoxFuture<'a, SoapResult<i32>> {
360            Box::pin(async move {
361                self.trace
362                    .lock()
363                    .map_err(|_| SoapError::infrastructure("command test trace lock poisoned"))?
364                    .push(self.before);
365                let output = next.run(command).await?;
366                self.trace
367                    .lock()
368                    .map_err(|_| SoapError::infrastructure("command test trace lock poisoned"))?
369                    .push(self.after);
370                Ok(output + self.add)
371            })
372        }
373    }
374
375    #[test]
376    fn command_pipeline_preserves_types_and_around_order() {
377        let trace = Arc::new(Mutex::new(Vec::new()));
378        let handler: Arc<dyn CommandHandler<Add>> = Arc::new(AddHandler {
379            trace: Arc::clone(&trace),
380        });
381        let pipeline = CommandPipeline::new(handler)
382            .with_middleware(Arc::new(AroundCommand {
383                before: "outer-before",
384                after: "outer-after",
385                add: 1,
386                trace: Arc::clone(&trace),
387            }))
388            .with_middleware(Arc::new(AroundCommand {
389                before: "inner-before",
390                after: "inner-after",
391                add: 10,
392                trace: Arc::clone(&trace),
393            }));
394
395        assert_eq!(pipeline.middleware_count(), 2);
396        assert_eq!(block_on(pipeline.dispatch(Add(5))).ok(), Some(16));
397        assert_eq!(
398            trace.lock().ok().map(|items| items.clone()),
399            Some(vec![
400                "outer-before",
401                "inner-before",
402                "handler",
403                "inner-after",
404                "outer-after"
405            ])
406        );
407    }
408
409    struct RejectCommand;
410
411    impl CommandMiddleware<Add> for RejectCommand {
412        fn handle<'a>(
413            &'a self,
414            _command: Add,
415            _next: CommandNext<'a, Add>,
416        ) -> BoxFuture<'a, SoapResult<i32>> {
417            Box::pin(async { Err(SoapError::validation("command rejected by middleware")) })
418        }
419    }
420
421    #[test]
422    fn command_middleware_can_short_circuit_the_handler() {
423        let trace = Arc::new(Mutex::new(Vec::new()));
424        let handler: Arc<dyn CommandHandler<Add>> = Arc::new(AddHandler {
425            trace: Arc::clone(&trace),
426        });
427        let pipeline = CommandPipeline::new(handler).with_middleware(Arc::new(RejectCommand));
428
429        let result = block_on(pipeline.dispatch(Add(5)));
430        assert_eq!(
431            result.as_ref().map_err(SoapError::kind),
432            Err(SoapErrorKind::Validation)
433        );
434        assert_eq!(trace.lock().ok().map(|items| items.is_empty()), Some(true));
435    }
436
437    struct Double(i32);
438
439    impl Query for Double {
440        type Output = i32;
441    }
442
443    struct DoubleHandler;
444
445    impl QueryHandler<Double> for DoubleHandler {
446        fn query(&self, query: Double) -> BoxFuture<'_, SoapResult<i32>> {
447            Box::pin(async move { Ok(query.0 * 2) })
448        }
449    }
450
451    struct AddToQuery(i32);
452
453    impl QueryMiddleware<Double> for AddToQuery {
454        fn handle<'a>(
455            &'a self,
456            query: Double,
457            next: QueryNext<'a, Double>,
458        ) -> BoxFuture<'a, SoapResult<i32>> {
459            Box::pin(async move { Ok(next.run(query).await? + self.0) })
460        }
461    }
462
463    #[test]
464    fn query_pipeline_and_direct_handlers_share_typed_dispatch() {
465        assert_eq!(block_on(DoubleHandler.dispatch(Double(4))).ok(), Some(8));
466
467        let handler: Arc<dyn QueryHandler<Double>> = Arc::new(DoubleHandler);
468        let mut pipeline = QueryPipeline::new(handler);
469        pipeline.push_middleware(Arc::new(AddToQuery(3)));
470        assert_eq!(pipeline.middleware_count(), 1);
471        assert_eq!(block_on(pipeline.dispatch(Double(4))).ok(), Some(11));
472    }
473}