Skip to main content

nexus_rt/
combinator.rs

1//! Handler combinators for fan-out dispatch.
2//!
3//! Combinators compose multiple handlers into a single [`Handler`]
4//! that dispatches the same event to all of them by reference.
5//!
6//! - [`FanOut<T>`] — static fan-out. `T` is a tuple of handlers,
7//!   each receiving `&E`. Macro-generated for arities 2-8. Zero
8//!   allocation, concrete types, monomorphizes to direct calls.
9//! - [`Broadcast<E>`] — dynamic fan-out. Stores `Vec<Box<dyn ...>>`
10//!   handlers. One heap allocation per handler, zero clones at
11//!   dispatch.
12//!
13//! Both combinators implement `Handler<E>` — they take ownership of
14//! the event, borrow it, and forward `&E` to each child handler.
15//!
16//! Handlers inside combinators must implement `for<'e> Handler<&'e E>`
17//! — they receive the event by reference. Use [`Cloned`](crate::Cloned)
18//! or [`Owned`](crate::Owned) to adapt owned-event handlers.
19//!
20//! # Examples
21//!
22//! ```
23//! use nexus_rt::{WorldBuilder, ResMut, IntoHandler, Handler, Resource};
24//! use nexus_rt::{fan_out, Broadcast, Cloned};
25//!
26//! #[derive(Resource)]
27//! struct SinkA(u64);
28//! #[derive(Resource)]
29//! struct SinkB(i64);
30//!
31//! fn write_a(mut sink: ResMut<SinkA>, event: &u32) {
32//!     sink.0 += *event as u64;
33//! }
34//!
35//! fn write_b(mut sink: ResMut<SinkB>, event: &u32) {
36//!     sink.0 += *event as i64;
37//! }
38//!
39//! let mut builder = WorldBuilder::new();
40//! builder.register(SinkA(0));
41//! builder.register(SinkB(0));
42//! let mut world = builder.build();
43//!
44//! // Static 2-way fan-out
45//! let h1 = write_a.into_handler(world.registry());
46//! let h2 = write_b.into_handler(world.registry());
47//! let mut fan = fan_out!(h1, h2);
48//! fan.run(&mut world, 5u32);
49//! assert_eq!(world.resource::<SinkA>().0, 5);
50//! assert_eq!(world.resource::<SinkB>().0, 5);
51//! ```
52
53use crate::Handler;
54use crate::world::World;
55
56// =============================================================================
57// fan_out! macro
58// =============================================================================
59
60/// Constructs a [`FanOut`] combinator from 2-8 handlers.
61///
62/// Syntactic sugar for `FanOut((h1, h2, ...))` — avoids the
63/// double-parentheses of tuple struct construction.
64///
65/// # Examples
66///
67/// ```
68/// use nexus_rt::{WorldBuilder, ResMut, IntoHandler, Handler, fan_out, Resource};
69///
70/// #[derive(Resource)]
71/// struct Counter(u64);
72///
73/// fn inc(mut n: ResMut<Counter>, event: &u32) { n.0 += *event as u64; }
74///
75/// let mut builder = WorldBuilder::new();
76/// builder.register(Counter(0));
77/// let mut world = builder.build();
78///
79/// let h1 = inc.into_handler(world.registry());
80/// let h2 = inc.into_handler(world.registry());
81/// let mut fan = fan_out!(h1, h2);
82/// fan.run(&mut world, 1u32);
83/// assert_eq!(world.resource::<Counter>().0, 2);
84/// ```
85#[macro_export]
86macro_rules! fan_out {
87    ($handler:expr $(,)?) => {
88        compile_error!("fan_out! requires at least 2 handlers");
89    };
90    ($($handler:expr),+ $(,)?) => {
91        $crate::FanOut(($($handler,)+))
92    };
93}
94
95// =============================================================================
96// FanOut<T> — static tuple fan-out
97// =============================================================================
98
99/// Static fan-out combinator. Takes ownership of an event, borrows it,
100/// and dispatches `&E` to N handlers.
101///
102/// `T` is a tuple of handlers — construct via the [`fan_out!`] macro
103/// or directly: `FanOut((a, b))`. Macro-generated [`Handler<E>`]
104/// impls for tuple arities 2 through 8.
105///
106/// Each handler in the tuple must implement `for<'e> Handler<&'e E>`.
107/// To include an owned-event handler, wrap it in
108/// [`Cloned`](crate::Cloned) or [`Owned`](crate::Owned).
109///
110/// Zero allocation, concrete types — monomorphizes to direct calls.
111/// Boxes into `Box<dyn Handler<E>>` for type-erased storage.
112///
113/// For dynamic fan-out (runtime-determined handler count), use
114/// [`Broadcast`].
115///
116/// # Examples
117///
118/// ```
119/// use nexus_rt::{WorldBuilder, ResMut, IntoHandler, Handler, FanOut, Cloned, Resource};
120///
121/// #[derive(Resource)]
122/// struct Counter(u64);
123///
124/// fn ref_handler(mut sink: ResMut<Counter>, event: &u32) {
125///     sink.0 += *event as u64;
126/// }
127///
128/// fn owned_handler(mut sink: ResMut<Counter>, event: u32) {
129///     sink.0 += event as u64 * 10;
130/// }
131///
132/// let mut builder = WorldBuilder::new();
133/// builder.register(Counter(0));
134/// let mut world = builder.build();
135///
136/// // Mix ref and owned handlers via Cloned adapter
137/// let h1 = ref_handler.into_handler(world.registry());
138/// let h2 = owned_handler.into_handler(world.registry());
139/// let mut fan = FanOut((h1, Cloned(h2)));
140/// fan.run(&mut world, 3u32);
141/// assert_eq!(world.resource::<Counter>().0, 33); // 3 + 30
142/// ```
143pub struct FanOut<T>(pub T);
144
145macro_rules! impl_fanout {
146    ($($idx:tt: $H:ident),+) => {
147        impl<E, $($H),+> Handler<E> for FanOut<($($H,)+)>
148        where
149            $($H: for<'e> Handler<&'e E> + Send,)+
150        {
151            fn run(&mut self, world: &mut World, event: E) {
152                $(self.0.$idx.run(world, &event);)+
153            }
154
155            fn name(&self) -> &'static str {
156                "FanOut"
157            }
158        }
159    };
160}
161
162impl_fanout!(0: H0, 1: H1);
163impl_fanout!(0: H0, 1: H1, 2: H2);
164impl_fanout!(0: H0, 1: H1, 2: H2, 3: H3);
165impl_fanout!(0: H0, 1: H1, 2: H2, 3: H3, 4: H4);
166impl_fanout!(0: H0, 1: H1, 2: H2, 3: H3, 4: H4, 5: H5);
167impl_fanout!(0: H0, 1: H1, 2: H2, 3: H3, 4: H4, 5: H5, 6: H6);
168impl_fanout!(0: H0, 1: H1, 2: H2, 3: H3, 4: H4, 5: H5, 6: H6, 7: H7);
169
170// =============================================================================
171// Broadcast<E> — dynamic fan-out
172// =============================================================================
173
174/// Object-safe helper trait that erases the HRTB lifetime from
175/// `for<'e> Handler<&'e E>`.
176///
177/// Rust does not allow `Box<dyn for<'a> Handler<&'a E>>` directly.
178/// This trait bridges the gap: any `H: for<'e> Handler<&'e E>`
179/// gets a blanket `RefHandler<E>` impl, and [`Broadcast`] stores
180/// `Box<dyn RefHandler<E>>`.
181///
182/// Only `run_ref` is needed — [`Broadcast::name`] returns a fixed
183/// string since it wraps N heterogeneous handlers.
184trait RefHandler<E>: Send {
185    fn run_ref(&mut self, world: &mut World, event: &E);
186}
187
188impl<E, H> RefHandler<E> for H
189where
190    H: for<'e> Handler<&'e E> + Send,
191{
192    fn run_ref(&mut self, world: &mut World, event: &E) {
193        self.run(world, event);
194    }
195}
196
197/// Dynamic fan-out combinator. Takes ownership of an event, borrows
198/// it, and dispatches `&E` to N handlers, where N is determined at
199/// runtime.
200///
201/// One heap allocation per handler (boxing). Zero clones at dispatch
202/// — each handler receives `&E`.
203///
204/// Handlers must implement `for<'e> Handler<&'e E>`. Use
205/// [`Cloned`](crate::Cloned) or [`Owned`](crate::Owned) to adapt
206/// owned-event handlers.
207///
208/// For static fan-out (known handler count, zero allocation), use
209/// [`FanOut`].
210///
211/// # Examples
212///
213/// ```
214/// use nexus_rt::{WorldBuilder, ResMut, IntoHandler, Handler, Broadcast, Resource};
215///
216/// #[derive(Resource)]
217/// struct Counter(u64);
218///
219/// fn write_a(mut sink: ResMut<Counter>, event: &u32) {
220///     sink.0 += *event as u64;
221/// }
222///
223/// let mut builder = WorldBuilder::new();
224/// builder.register(Counter(0));
225/// let mut world = builder.build();
226///
227/// let mut broadcast: Broadcast<u32> = Broadcast::new();
228/// broadcast.add(write_a.into_handler(world.registry()));
229/// broadcast.add(write_a.into_handler(world.registry()));
230/// broadcast.run(&mut world, 5u32);
231/// assert_eq!(world.resource::<Counter>().0, 10);
232/// ```
233pub struct Broadcast<E> {
234    handlers: Vec<Box<dyn RefHandler<E>>>,
235}
236
237impl<E> Default for Broadcast<E> {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243impl<E> Broadcast<E> {
244    /// Create an empty broadcast with no handlers.
245    pub fn new() -> Self {
246        Self {
247            handlers: Vec::new(),
248        }
249    }
250
251    /// Add a handler to the broadcast.
252    pub fn add<H: for<'e> Handler<&'e E> + Send + 'static>(&mut self, handler: H) {
253        self.handlers.push(Box::new(handler));
254    }
255
256    /// Returns the number of handlers.
257    pub fn len(&self) -> usize {
258        self.handlers.len()
259    }
260
261    /// Returns `true` if there are no handlers.
262    pub fn is_empty(&self) -> bool {
263        self.handlers.is_empty()
264    }
265}
266
267impl<E> Handler<E> for Broadcast<E> {
268    fn run(&mut self, world: &mut World, event: E) {
269        for h in &mut self.handlers {
270            h.run_ref(world, &event);
271        }
272    }
273
274    fn name(&self) -> &'static str {
275        "Broadcast"
276    }
277}
278
279// =============================================================================
280// Tests
281// =============================================================================
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use crate::{Cloned, IntoHandler, ResMut, WorldBuilder};
287
288    fn write_u64(mut sink: ResMut<u64>, event: &u32) {
289        *sink += *event as u64;
290    }
291
292    fn write_i64(mut sink: ResMut<i64>, event: &u32) {
293        *sink += *event as i64 * 2;
294    }
295
296    fn write_f64(mut sink: ResMut<f64>, event: &u32) {
297        *sink += *event as f64 * 0.5;
298    }
299
300    fn owned_handler(mut sink: ResMut<u64>, event: u32) {
301        *sink += event as u64 * 10;
302    }
303
304    // -- FanOut ---------------------------------------------------------------
305
306    #[test]
307    fn fanout_two_way() {
308        let mut builder = WorldBuilder::new();
309        builder.register::<u64>(0);
310        builder.register::<i64>(0);
311        let mut world = builder.build();
312
313        let h1 = write_u64.into_handler(world.registry());
314        let h2 = write_i64.into_handler(world.registry());
315        let mut fan = fan_out!(h1, h2);
316        fan.run(&mut world, 5u32);
317        assert_eq!(*world.resource::<u64>(), 5);
318        assert_eq!(*world.resource::<i64>(), 10);
319    }
320
321    #[test]
322    fn fanout_three_way() {
323        let mut builder = WorldBuilder::new();
324        builder.register::<u64>(0);
325        builder.register::<i64>(0);
326        builder.register::<f64>(0.0);
327        let mut world = builder.build();
328
329        let h1 = write_u64.into_handler(world.registry());
330        let h2 = write_i64.into_handler(world.registry());
331        let h3 = write_f64.into_handler(world.registry());
332        let mut fan = fan_out!(h1, h2, h3);
333        fan.run(&mut world, 10u32);
334        assert_eq!(*world.resource::<u64>(), 10);
335        assert_eq!(*world.resource::<i64>(), 20);
336        #[allow(clippy::float_cmp)]
337        {
338            assert_eq!(*world.resource::<f64>(), 5.0);
339        }
340    }
341
342    #[test]
343    fn fanout_with_cloned_adapter() {
344        let mut builder = WorldBuilder::new();
345        builder.register::<u64>(0);
346        let mut world = builder.build();
347
348        let ref_h = write_u64.into_handler(world.registry());
349        let owned_h = owned_handler.into_handler(world.registry());
350        let mut fan = fan_out!(ref_h, Cloned(owned_h));
351        fan.run(&mut world, 3u32);
352        assert_eq!(*world.resource::<u64>(), 33); // 3 + 30
353    }
354
355    #[test]
356    fn fanout_boxable() {
357        let mut builder = WorldBuilder::new();
358        builder.register::<u64>(0);
359        builder.register::<i64>(0);
360        let mut world = builder.build();
361
362        let h1 = write_u64.into_handler(world.registry());
363        let h2 = write_i64.into_handler(world.registry());
364        let mut boxed: Box<dyn Handler<u32>> = Box::new(fan_out!(h1, h2));
365        boxed.run(&mut world, 7u32);
366        assert_eq!(*world.resource::<u64>(), 7);
367        assert_eq!(*world.resource::<i64>(), 14);
368    }
369
370    // -- Broadcast ------------------------------------------------------------
371
372    #[test]
373    fn broadcast_dispatch() {
374        let mut builder = WorldBuilder::new();
375        builder.register::<u64>(0);
376        let mut world = builder.build();
377
378        let mut broadcast: Broadcast<u32> = Broadcast::new();
379        broadcast.add(write_u64.into_handler(world.registry()));
380        broadcast.add(write_u64.into_handler(world.registry()));
381        broadcast.add(write_u64.into_handler(world.registry()));
382        broadcast.run(&mut world, 4u32);
383        assert_eq!(*world.resource::<u64>(), 12); // 4 + 4 + 4
384    }
385
386    #[test]
387    fn broadcast_empty() {
388        let mut builder = WorldBuilder::new();
389        builder.register::<u64>(0);
390        let mut world = builder.build();
391
392        let mut broadcast: Broadcast<u32> = Broadcast::new();
393        assert!(broadcast.is_empty());
394        broadcast.run(&mut world, 1u32);
395        assert_eq!(*world.resource::<u64>(), 0);
396    }
397
398    #[test]
399    fn broadcast_len() {
400        let mut builder = WorldBuilder::new();
401        builder.register::<u64>(0);
402        let world = builder.build();
403
404        let mut broadcast: Broadcast<u32> = Broadcast::new();
405        assert_eq!(broadcast.len(), 0);
406        broadcast.add(write_u64.into_handler(world.registry()));
407        assert_eq!(broadcast.len(), 1);
408        broadcast.add(write_u64.into_handler(world.registry()));
409        assert_eq!(broadcast.len(), 2);
410    }
411
412    #[test]
413    fn broadcast_with_cloned_adapter() {
414        let mut builder = WorldBuilder::new();
415        builder.register::<u64>(0);
416        let mut world = builder.build();
417
418        let mut broadcast: Broadcast<u32> = Broadcast::new();
419        broadcast.add(write_u64.into_handler(world.registry()));
420        broadcast.add(Cloned(owned_handler.into_handler(world.registry())));
421        broadcast.run(&mut world, 2u32);
422        assert_eq!(*world.resource::<u64>(), 22); // 2 + 20
423    }
424}