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