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}