Skip to main content

nexus_rt/
template.rs

1//! Resolve-once, stamp-many handler templates.
2//!
3//! Templates separate handler *creation cost* (registry lookup, access
4//! validation) from *instantiation*. A [`HandlerTemplate`] resolves
5//! parameters once, then [`generate`](HandlerTemplate::generate) stamps
6//! out [`TemplatedHandler`]s by copying the pre-resolved state.
7//!
8//! Same pattern for context-owning handlers: [`CallbackTemplate`] +
9//! [`TemplatedCallback`].
10//!
11//! # Constraints
12//!
13//! - **`P::State: Copy`** — required for `generate()` to stamp copies.
14//!   This effectively rules out [`Local<T>`](crate::Local) when the
15//!   per-instance state is not `Copy` (non-duplicable state is
16//!   incompatible with template stamping). All World-backed params
17//!   ([`Res`](crate::Res), [`ResMut`](crate::ResMut), `Option` variants)
18//!   have `State = ResourceId` which is `Copy`.
19//! - **Zero-sized, non-capturing callables only** — `F` must be a ZST
20//!   (zero-sized type). Capturing closures and function pointers are
21//!   rejected at compile time; captureless closures and function items
22//!   are allowed.
23//!
24//! # Examples
25//!
26//! ```
27//! use nexus_rt::{WorldBuilder, ResMut, Handler};
28//! use nexus_rt::template::{Blueprint, HandlerTemplate};
29//!
30//! struct OnTick;
31//! impl Blueprint for OnTick {
32//!     type Event = u32;
33//!     type Params = (ResMut<'static, u64>,);
34//! }
35//!
36//! fn tick(mut counter: ResMut<u64>, event: u32) {
37//!     *counter += event as u64;
38//! }
39//!
40//! let mut builder = WorldBuilder::new();
41//! builder.register::<u64>(0);
42//! let world = builder.build();
43//!
44//! let template = HandlerTemplate::<OnTick>::new(tick, world.registry());
45//! let mut h1 = template.generate();
46//! let mut h2 = template.generate();
47//!
48//! // Both share pre-resolved state — no redundant registry lookups.
49//! ```
50
51use std::marker::PhantomData;
52
53use crate::Handler;
54use crate::handler::Param;
55use crate::world::{Registry, World};
56
57// =============================================================================
58// Blueprint traits
59// =============================================================================
60
61/// Declares a handler template's event and parameter types.
62///
63/// The implementing struct is a *key* — a zero-sized marker that names
64/// the template. No `'static` bound on `Event` because wire protocols
65/// (e.g. Aeron) pass events borrowed from term buffers.
66///
67/// # Examples
68///
69/// ```
70/// use nexus_rt::template::Blueprint;
71/// use nexus_rt::ResMut;
72///
73/// struct OnTick;
74/// impl Blueprint for OnTick {
75///     type Event = u32;
76///     type Params = (ResMut<'static, u64>,);
77/// }
78/// ```
79///
80/// The `'static` on `Res`/`ResMut` is a naming requirement (GATs need
81/// a concrete lifetime). It is never used at runtime — `fetch()` binds
82/// a real lifetime at each call site.
83pub trait Blueprint {
84    /// The event type dispatched to handlers stamped from this template.
85    type Event;
86    /// The parameter tuple. Must satisfy [`Param`] + `'static`.
87    type Params: Param + 'static;
88}
89
90/// Extension of [`Blueprint`] for context-owning handlers.
91///
92/// Adds the per-instance context type `C` that each
93/// [`TemplatedCallback`] carries.
94pub trait CallbackBlueprint: Blueprint {
95    /// Per-instance owned context type.
96    type Context: Send + 'static;
97}
98
99// =============================================================================
100// TemplateDispatch — doc-hidden, per-arity fn ptr provider
101// =============================================================================
102
103/// Bridges arity-independent template structs with arity-dependent
104/// dispatch logic. Generated by `all_tuples!`.
105///
106/// Each arity provides a fn ptr that performs `Param::fetch` + call,
107/// and a `validate` method that checks for conflicting resource access.
108#[doc(hidden)]
109pub trait TemplateDispatch<P: Param, E> {
110    /// Returns a fn ptr that fetches params and calls the handler.
111    fn run_fn_ptr() -> unsafe fn(&mut P::State, &mut World, E);
112    /// Validates resource access (panics on conflict).
113    fn validate(state: &P::State, registry: &Registry);
114}
115
116/// Context-owning variant of [`TemplateDispatch`].
117#[doc(hidden)]
118pub trait CallbackTemplateDispatch<C, P: Param, E> {
119    /// Returns a fn ptr that fetches params and calls the handler with context.
120    fn run_fn_ptr() -> unsafe fn(&mut C, &mut P::State, &mut World, E);
121    /// Validates resource access (panics on conflict).
122    fn validate(state: &P::State, registry: &Registry);
123}
124
125// =============================================================================
126// Arity 0: event-only handlers
127// =============================================================================
128
129impl<E, F: FnMut(E) + Send + 'static> TemplateDispatch<(), E> for F {
130    fn run_fn_ptr() -> unsafe fn(&mut (), &mut World, E) {
131        /// # Safety
132        ///
133        /// `F` must be a ZST (enforced by `HandlerTemplate::new`).
134        /// `std::mem::zeroed()` on a ZST is sound — no bytes to initialize.
135        unsafe fn run<E, F: FnMut(E) + Send>(_state: &mut (), _world: &mut World, event: E) {
136            // SAFETY: F is ZST (asserted in HandlerTemplate::new).
137            // A ZST has no data — zeroed() produces the unique value.
138            let mut f: F = unsafe { std::mem::zeroed() };
139            f(event);
140        }
141        run::<E, F>
142    }
143
144    fn validate(_state: &(), _registry: &Registry) {}
145}
146
147impl<C: Send + 'static, E, F: FnMut(&mut C, E) + Send + 'static> CallbackTemplateDispatch<C, (), E>
148    for F
149{
150    fn run_fn_ptr() -> unsafe fn(&mut C, &mut (), &mut World, E) {
151        unsafe fn run<C: Send, E, F: FnMut(&mut C, E) + Send>(
152            ctx: &mut C,
153            _state: &mut (),
154            _world: &mut World,
155            event: E,
156        ) {
157            // SAFETY: F is ZST — see arity-0 TemplateDispatch.
158            let mut f: F = unsafe { std::mem::zeroed() };
159            f(ctx, event);
160        }
161        run::<C, E, F>
162    }
163
164    fn validate(_state: &(), _registry: &Registry) {}
165}
166
167// =============================================================================
168// Per-arity TemplateDispatch / CallbackTemplateDispatch via macro
169// =============================================================================
170
171macro_rules! impl_template_dispatch {
172    ($($P:ident),+) => {
173        impl<E, F: Send + 'static, $($P: Param + 'static),+> TemplateDispatch<($($P,)+), E> for F
174        where
175            for<'a> &'a mut F: FnMut($($P,)+ E) + FnMut($($P::Item<'a>,)+ E),
176        {
177            fn run_fn_ptr() -> unsafe fn(&mut ($($P::State,)+), &mut World, E) {
178                /// # Safety
179                ///
180                /// - `F` must be a ZST (enforced by `HandlerTemplate::new`).
181                /// - `state` must have been produced by `Param::init` on the
182                ///   same registry that built `world`.
183                /// - Caller ensures no mutable aliasing across params.
184                #[allow(non_snake_case)]
185                unsafe fn run<E, F: Send + 'static, $($P: Param + 'static),+>(
186                    state: &mut ($($P::State,)+),
187                    world: &mut World,
188                    event: E,
189                ) where
190                    for<'a> &'a mut F: FnMut($($P,)+ E) + FnMut($($P::Item<'a>,)+ E),
191                {
192                    #[allow(clippy::too_many_arguments)]
193                    fn call_inner<$($P,)+ Ev>(
194                        mut f: impl FnMut($($P,)+ Ev),
195                        $($P: $P,)+
196                        event: Ev,
197                    ) {
198                        f($($P,)+ event);
199                    }
200
201                    // SAFETY: state produced by init() on same registry.
202                    // Single-threaded sequential dispatch — no mutable aliasing.
203                    #[cfg(debug_assertions)]
204                    world.clear_borrows();
205                    let ($($P,)+) = unsafe {
206                        <($($P,)+) as Param>::fetch(world, state)
207                    };
208                    // SAFETY: F is ZST — zeroed() produces the unique value.
209                    let mut f: F = unsafe { std::mem::zeroed() };
210                    call_inner(&mut f, $($P,)+ event);
211                }
212                run::<E, F, $($P),+>
213            }
214
215            #[allow(non_snake_case)]
216            fn validate(state: &($($P::State,)+), registry: &Registry) {
217                let ($($P,)+) = state;
218                registry.check_access(&[
219                    $((<$P as Param>::resource_id($P), std::any::type_name::<$P>()),)+
220                ]);
221            }
222        }
223
224        impl<C: Send + 'static, E, F: Send + 'static, $($P: Param + 'static),+>
225            CallbackTemplateDispatch<C, ($($P,)+), E> for F
226        where
227            for<'a> &'a mut F:
228                FnMut(&mut C, $($P,)+ E) +
229                FnMut(&mut C, $($P::Item<'a>,)+ E),
230        {
231            fn run_fn_ptr() -> unsafe fn(&mut C, &mut ($($P::State,)+), &mut World, E) {
232                #[allow(non_snake_case)]
233                unsafe fn run<C: Send, E, F: Send + 'static, $($P: Param + 'static),+>(
234                    ctx: &mut C,
235                    state: &mut ($($P::State,)+),
236                    world: &mut World,
237                    event: E,
238                ) where
239                    for<'a> &'a mut F:
240                        FnMut(&mut C, $($P,)+ E) +
241                        FnMut(&mut C, $($P::Item<'a>,)+ E),
242                {
243                    #[allow(clippy::too_many_arguments)]
244                    fn call_inner<Ctx, $($P,)+ Ev>(
245                        mut f: impl FnMut(&mut Ctx, $($P,)+ Ev),
246                        ctx: &mut Ctx,
247                        $($P: $P,)+
248                        event: Ev,
249                    ) {
250                        f(ctx, $($P,)+ event);
251                    }
252
253                    #[cfg(debug_assertions)]
254                    world.clear_borrows();
255                    let ($($P,)+) = unsafe {
256                        <($($P,)+) as Param>::fetch(world, state)
257                    };
258                    let mut f: F = unsafe { std::mem::zeroed() };
259                    call_inner(&mut f, ctx, $($P,)+ event);
260                }
261                run::<C, E, F, $($P),+>
262            }
263
264            #[allow(non_snake_case)]
265            fn validate(state: &($($P::State,)+), registry: &Registry) {
266                let ($($P,)+) = state;
267                registry.check_access(&[
268                    $((<$P as Param>::resource_id($P), std::any::type_name::<$P>()),)+
269                ]);
270            }
271        }
272    };
273}
274
275macro_rules! all_tuples {
276    ($m:ident) => {
277        $m!(P0);
278        $m!(P0, P1);
279        $m!(P0, P1, P2);
280        $m!(P0, P1, P2, P3);
281        $m!(P0, P1, P2, P3, P4);
282        $m!(P0, P1, P2, P3, P4, P5);
283        $m!(P0, P1, P2, P3, P4, P5, P6);
284        $m!(P0, P1, P2, P3, P4, P5, P6, P7);
285    };
286}
287
288all_tuples!(impl_template_dispatch);
289
290// =============================================================================
291// HandlerTemplate<K>
292// =============================================================================
293
294/// Resolve-once template for context-free handlers.
295///
296/// Created from a named function + [`Registry`]. Calling
297/// [`generate`](Self::generate) stamps out [`TemplatedHandler`]s by
298/// copying pre-resolved state — no registry lookups, no validation.
299///
300/// # Examples
301///
302/// ```
303/// use nexus_rt::{WorldBuilder, ResMut, Handler};
304/// use nexus_rt::template::{Blueprint, HandlerTemplate};
305///
306/// struct OnTick;
307/// impl Blueprint for OnTick {
308///     type Event = u32;
309///     type Params = (ResMut<'static, u64>,);
310/// }
311///
312/// fn tick(mut counter: ResMut<u64>, event: u32) {
313///     *counter += event as u64;
314/// }
315///
316/// let mut builder = WorldBuilder::new();
317/// builder.register::<u64>(0);
318/// let world = builder.build();
319///
320/// let template = HandlerTemplate::<OnTick>::new(tick, world.registry());
321///
322/// let mut h1 = template.generate();
323/// let mut h2 = template.generate();
324///
325/// // Both share pre-resolved ResourceIds — no redundant lookups.
326/// ```
327pub struct HandlerTemplate<K: Blueprint>
328where
329    <K::Params as Param>::State: Copy,
330{
331    prototype: <K::Params as Param>::State,
332    run_fn: unsafe fn(&mut <K::Params as Param>::State, &mut World, K::Event),
333    name: &'static str,
334    // fn() -> K avoids propagating K's Send/Sync bounds through PhantomData.
335    _key: PhantomData<fn() -> K>,
336}
337
338impl<K: Blueprint> HandlerTemplate<K>
339where
340    <K::Params as Param>::State: Copy,
341{
342    /// Create a template from a named function.
343    ///
344    /// Resolves all parameters from the registry and validates access.
345    /// `F` must be a ZST (named function item) — closures and function
346    /// pointers are rejected at compile time.
347    ///
348    /// # Panics
349    ///
350    /// - If any required resource is not registered.
351    /// - If params have conflicting access (same resource twice).
352    #[allow(clippy::needless_pass_by_value)]
353    pub fn new<F>(f: F, registry: &Registry) -> Self
354    where
355        F: TemplateDispatch<K::Params, K::Event>,
356    {
357        const {
358            assert!(
359                std::mem::size_of::<F>() == 0,
360                "F must be a ZST (named function item, not a closure or fn pointer)"
361            );
362        }
363        let _ = f;
364        let prototype = K::Params::init(registry);
365        F::validate(&prototype, registry);
366        Self {
367            prototype,
368            run_fn: F::run_fn_ptr(),
369            name: std::any::type_name::<F>(),
370            _key: PhantomData,
371        }
372    }
373
374    /// Stamp out a new handler by copying pre-resolved state.
375    pub fn generate(&self) -> TemplatedHandler<K> {
376        TemplatedHandler {
377            state: self.prototype,
378            run_fn: self.run_fn,
379            name: self.name,
380            _key: PhantomData,
381        }
382    }
383}
384
385// =============================================================================
386// TemplatedHandler<K>
387// =============================================================================
388
389/// A handler stamped from a [`HandlerTemplate`].
390///
391/// Implements [`Handler<K::Event>`] for direct or boxed dispatch.
392/// No registry lookups — all state was pre-resolved by the template.
393pub struct TemplatedHandler<K: Blueprint>
394where
395    <K::Params as Param>::State: Copy,
396{
397    state: <K::Params as Param>::State,
398    run_fn: unsafe fn(&mut <K::Params as Param>::State, &mut World, K::Event),
399    name: &'static str,
400    _key: PhantomData<fn() -> K>,
401}
402
403impl<K: Blueprint> Handler<K::Event> for TemplatedHandler<K>
404where
405    <K::Params as Param>::State: Copy,
406{
407    fn run(&mut self, world: &mut World, event: K::Event) {
408        // SAFETY: run_fn was produced by TemplateDispatch for the matching
409        // F + K combination. State was produced by Param::init on the same
410        // registry that built world.
411        unsafe { (self.run_fn)(&mut self.state, world, event) }
412    }
413
414    fn name(&self) -> &'static str {
415        self.name
416    }
417}
418
419// =============================================================================
420// CallbackTemplate<K>
421// =============================================================================
422
423/// Fn ptr type for context-owning dispatch (avoids `type_complexity` lint).
424type CallbackRunFn<K> = unsafe fn(
425    &mut <K as CallbackBlueprint>::Context,
426    &mut <<K as Blueprint>::Params as Param>::State,
427    &mut World,
428    <K as Blueprint>::Event,
429);
430
431/// Resolve-once template for context-owning handlers.
432///
433/// Like [`HandlerTemplate`], but each [`generate`](Self::generate)
434/// call takes a `K::Context` to produce a [`TemplatedCallback`] with
435/// per-instance owned state.
436///
437/// # Examples
438///
439/// ```
440/// use nexus_rt::{WorldBuilder, ResMut, Handler};
441/// use nexus_rt::template::{Blueprint, CallbackBlueprint, CallbackTemplate};
442///
443/// struct TimerCtx { order_id: u64, fires: u64 }
444///
445/// struct OnTimeout;
446/// impl Blueprint for OnTimeout {
447///     type Event = ();
448///     type Params = (ResMut<'static, u64>,);
449/// }
450/// impl CallbackBlueprint for OnTimeout {
451///     type Context = TimerCtx;
452/// }
453///
454/// fn on_timeout(ctx: &mut TimerCtx, mut counter: ResMut<u64>, _event: ()) {
455///     ctx.fires += 1;
456///     *counter += ctx.order_id;
457/// }
458///
459/// let mut builder = WorldBuilder::new();
460/// builder.register::<u64>(0);
461/// let world = builder.build();
462///
463/// let template = CallbackTemplate::<OnTimeout>::new(on_timeout, world.registry());
464/// let mut cb1 = template.generate(TimerCtx { order_id: 10, fires: 0 });
465/// let mut cb2 = template.generate(TimerCtx { order_id: 20, fires: 0 });
466///
467/// // Each carries its own context, shares pre-resolved state.
468/// ```
469pub struct CallbackTemplate<K: CallbackBlueprint>
470where
471    <K::Params as Param>::State: Copy,
472{
473    prototype: <K::Params as Param>::State,
474    run_fn: CallbackRunFn<K>,
475    name: &'static str,
476    _key: PhantomData<fn() -> K>,
477}
478
479impl<K: CallbackBlueprint> CallbackTemplate<K>
480where
481    <K::Params as Param>::State: Copy,
482{
483    /// Create a template from a named function.
484    ///
485    /// Same constraints as [`HandlerTemplate::new`]: `F` must be ZST,
486    /// all resources must be registered.
487    ///
488    /// # Panics
489    ///
490    /// - If any required resource is not registered.
491    /// - If params have conflicting access.
492    #[allow(clippy::needless_pass_by_value)]
493    pub fn new<F>(f: F, registry: &Registry) -> Self
494    where
495        F: CallbackTemplateDispatch<K::Context, K::Params, K::Event>,
496    {
497        const {
498            assert!(
499                std::mem::size_of::<F>() == 0,
500                "F must be a ZST (named function item, not a closure or fn pointer)"
501            );
502        }
503        let _ = f;
504        let prototype = K::Params::init(registry);
505        F::validate(&prototype, registry);
506        Self {
507            prototype,
508            run_fn: F::run_fn_ptr(),
509            name: std::any::type_name::<F>(),
510            _key: PhantomData,
511        }
512    }
513
514    /// Stamp out a new callback with the given context.
515    pub fn generate(&self, ctx: K::Context) -> TemplatedCallback<K> {
516        TemplatedCallback {
517            ctx,
518            state: self.prototype,
519            run_fn: self.run_fn,
520            name: self.name,
521            _key: PhantomData,
522        }
523    }
524}
525
526// =============================================================================
527// TemplatedCallback<K>
528// =============================================================================
529
530/// A callback stamped from a [`CallbackTemplate`].
531///
532/// Implements [`Handler<K::Event>`].
533pub struct TemplatedCallback<K: CallbackBlueprint>
534where
535    <K::Params as Param>::State: Copy,
536{
537    ctx: K::Context,
538    state: <K::Params as Param>::State,
539    run_fn: CallbackRunFn<K>,
540    name: &'static str,
541    _key: PhantomData<fn() -> K>,
542}
543
544impl<K: CallbackBlueprint> TemplatedCallback<K>
545where
546    <K::Params as Param>::State: Copy,
547{
548    /// Returns a shared reference to the per-instance context.
549    pub fn ctx(&self) -> &K::Context {
550        &self.ctx
551    }
552
553    /// Returns an exclusive reference to the per-instance context.
554    pub fn ctx_mut(&mut self) -> &mut K::Context {
555        &mut self.ctx
556    }
557}
558
559impl<K: CallbackBlueprint> Handler<K::Event> for TemplatedCallback<K>
560where
561    <K::Params as Param>::State: Copy,
562{
563    fn run(&mut self, world: &mut World, event: K::Event) {
564        // SAFETY: run_fn was produced by CallbackTemplateDispatch for the
565        // matching F + K combination. State from Param::init on same registry.
566        unsafe { (self.run_fn)(&mut self.ctx, &mut self.state, world, event) }
567    }
568
569    fn name(&self) -> &'static str {
570        self.name
571    }
572}
573
574// =============================================================================
575// Convenience macros
576// =============================================================================
577
578/// Declares a handler [`Blueprint`] key struct.
579///
580/// # Examples
581///
582/// ```ignore
583/// handler_blueprint!(OnTick, Event = u32, Params = (Res<'static, u64>, ResMut<'static, bool>));
584/// ```
585///
586/// Expands to a unit struct + `Blueprint` impl. The `'static` on
587/// `Res`/`ResMut` is required (declarative macro limitation). Future
588/// proc macros will inject it automatically.
589#[macro_export]
590macro_rules! handler_blueprint {
591    ($name:ident, Event = $event:ty, Params = $params:ty) => {
592        struct $name;
593        impl $crate::template::Blueprint for $name {
594            type Event = $event;
595            type Params = $params;
596        }
597    };
598}
599
600/// Declares a callback [`CallbackBlueprint`] key struct.
601///
602/// # Examples
603///
604/// ```ignore
605/// callback_blueprint!(OnConn, Context = ConnState, Event = u32, Params = (ResMut<'static, u64>,));
606/// ```
607#[macro_export]
608macro_rules! callback_blueprint {
609    ($name:ident, Context = $ctx:ty, Event = $event:ty, Params = $params:ty) => {
610        struct $name;
611        impl $crate::template::Blueprint for $name {
612            type Event = $event;
613            type Params = $params;
614        }
615        impl $crate::template::CallbackBlueprint for $name {
616            type Context = $ctx;
617        }
618    };
619}
620
621// =============================================================================
622// Tests
623// =============================================================================
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628    use crate::{Res, ResMut, WorldBuilder};
629
630    // -- Blueprint definitions ------------------------------------------------
631
632    struct OnTick;
633    impl Blueprint for OnTick {
634        type Event = u32;
635        type Params = (ResMut<'static, u64>,);
636    }
637
638    struct EventOnly;
639    impl Blueprint for EventOnly {
640        type Event = u32;
641        type Params = ();
642    }
643
644    struct TwoParams;
645    impl Blueprint for TwoParams {
646        type Event = ();
647        type Params = (Res<'static, u64>, ResMut<'static, bool>);
648    }
649
650    // -- HandlerTemplate tests ------------------------------------------------
651
652    fn tick(mut counter: ResMut<u64>, event: u32) {
653        *counter += event as u64;
654    }
655
656    #[test]
657    fn handler_template_basic() {
658        let mut builder = WorldBuilder::new();
659        builder.register::<u64>(0);
660        let mut world = builder.build();
661
662        let template = HandlerTemplate::<OnTick>::new(tick, world.registry());
663        let mut h = template.generate();
664        h.run(&mut world, 10);
665        assert_eq!(*world.resource::<u64>(), 10);
666    }
667
668    #[test]
669    fn handler_template_stamps_independent() {
670        let mut builder = WorldBuilder::new();
671        builder.register::<u64>(0);
672        let mut world = builder.build();
673
674        let template = HandlerTemplate::<OnTick>::new(tick, world.registry());
675        let mut h1 = template.generate();
676        let mut h2 = template.generate();
677
678        h1.run(&mut world, 10);
679        h2.run(&mut world, 5);
680        assert_eq!(*world.resource::<u64>(), 15);
681    }
682
683    fn event_only_fn(event: u32) {
684        assert!(event > 0);
685    }
686
687    #[test]
688    fn handler_template_event_only() {
689        let mut world = WorldBuilder::new().build();
690        let template = HandlerTemplate::<EventOnly>::new(event_only_fn, world.registry());
691        let mut h = template.generate();
692        h.run(&mut world, 42);
693    }
694
695    fn two_params_fn(counter: Res<u64>, mut flag: ResMut<bool>, _event: ()) {
696        if *counter > 0 {
697            *flag = true;
698        }
699    }
700
701    #[test]
702    fn handler_template_two_params() {
703        let mut builder = WorldBuilder::new();
704        builder.register::<u64>(1);
705        builder.register::<bool>(false);
706        let mut world = builder.build();
707
708        let template = HandlerTemplate::<TwoParams>::new(two_params_fn, world.registry());
709        let mut h = template.generate();
710        h.run(&mut world, ());
711        assert!(*world.resource::<bool>());
712    }
713
714    // -- Box<dyn Handler<E>> --------------------------------------------------
715
716    #[test]
717    fn templated_handler_boxable() {
718        let mut builder = WorldBuilder::new();
719        builder.register::<u64>(0);
720        let mut world = builder.build();
721
722        let template = HandlerTemplate::<OnTick>::new(tick, world.registry());
723        let h = template.generate();
724        let mut boxed: Box<dyn Handler<u32>> = Box::new(h);
725        boxed.run(&mut world, 7);
726        assert_eq!(*world.resource::<u64>(), 7);
727    }
728
729    // -- Name -----------------------------------------------------------------
730
731    #[test]
732    fn templated_handler_name() {
733        let mut builder = WorldBuilder::new();
734        builder.register::<u64>(0);
735        let world = builder.build();
736
737        let template = HandlerTemplate::<OnTick>::new(tick, world.registry());
738        let h = template.generate();
739        assert!(h.name().contains("tick"));
740    }
741
742    // -- Panics ---------------------------------------------------------------
743
744    #[test]
745    #[should_panic(expected = "not registered")]
746    fn handler_template_missing_resource() {
747        let world = WorldBuilder::new().build();
748        let _template = HandlerTemplate::<OnTick>::new(tick, world.registry());
749    }
750
751    #[test]
752    #[should_panic(expected = "conflicting access")]
753    fn handler_template_duplicate_access() {
754        struct BadBlueprint;
755        impl Blueprint for BadBlueprint {
756            type Event = ();
757            type Params = (Res<'static, u64>, ResMut<'static, u64>);
758        }
759
760        fn bad(a: Res<u64>, b: ResMut<u64>, _e: ()) {
761            let _ = (*a, &*b);
762        }
763
764        let mut builder = WorldBuilder::new();
765        builder.register::<u64>(0);
766        let world = builder.build();
767        let _template = HandlerTemplate::<BadBlueprint>::new(bad, world.registry());
768    }
769
770    // -- CallbackTemplate tests -----------------------------------------------
771
772    struct TimerCtx {
773        order_id: u64,
774        fires: u64,
775    }
776
777    struct OnTimeout;
778    impl Blueprint for OnTimeout {
779        type Event = ();
780        type Params = (ResMut<'static, u64>,);
781    }
782    impl CallbackBlueprint for OnTimeout {
783        type Context = TimerCtx;
784    }
785
786    fn on_timeout(ctx: &mut TimerCtx, mut counter: ResMut<u64>, _event: ()) {
787        ctx.fires += 1;
788        *counter += ctx.order_id;
789    }
790
791    #[test]
792    fn callback_template_basic() {
793        let mut builder = WorldBuilder::new();
794        builder.register::<u64>(0);
795        let mut world = builder.build();
796
797        let template = CallbackTemplate::<OnTimeout>::new(on_timeout, world.registry());
798        let mut cb = template.generate(TimerCtx {
799            order_id: 42,
800            fires: 0,
801        });
802        cb.run(&mut world, ());
803        assert_eq!(cb.ctx().fires, 1);
804        assert_eq!(*world.resource::<u64>(), 42);
805    }
806
807    #[test]
808    fn callback_template_independent_contexts() {
809        let mut builder = WorldBuilder::new();
810        builder.register::<u64>(0);
811        let mut world = builder.build();
812
813        let template = CallbackTemplate::<OnTimeout>::new(on_timeout, world.registry());
814        let mut cb1 = template.generate(TimerCtx {
815            order_id: 10,
816            fires: 0,
817        });
818        let mut cb2 = template.generate(TimerCtx {
819            order_id: 20,
820            fires: 0,
821        });
822
823        cb1.run(&mut world, ());
824        cb2.run(&mut world, ());
825        assert_eq!(cb1.ctx().fires, 1);
826        assert_eq!(cb2.ctx().fires, 1);
827        assert_eq!(*world.resource::<u64>(), 30);
828    }
829
830    struct CtxOnlyKey;
831    impl Blueprint for CtxOnlyKey {
832        type Event = u32;
833        type Params = ();
834    }
835    impl CallbackBlueprint for CtxOnlyKey {
836        type Context = u64;
837    }
838
839    fn ctx_only(ctx: &mut u64, event: u32) {
840        *ctx += event as u64;
841    }
842
843    #[test]
844    fn callback_template_event_only() {
845        let mut world = WorldBuilder::new().build();
846        let template = CallbackTemplate::<CtxOnlyKey>::new(ctx_only, world.registry());
847        let mut cb = template.generate(0u64);
848        cb.run(&mut world, 5);
849        assert_eq!(*cb.ctx(), 5);
850    }
851
852    #[test]
853    fn callback_template_boxable() {
854        let mut builder = WorldBuilder::new();
855        builder.register::<u64>(0);
856        let mut world = builder.build();
857
858        let template = CallbackTemplate::<OnTimeout>::new(on_timeout, world.registry());
859        let cb = template.generate(TimerCtx {
860            order_id: 7,
861            fires: 0,
862        });
863        let mut boxed: Box<dyn Handler<()>> = Box::new(cb);
864        boxed.run(&mut world, ());
865        assert_eq!(*world.resource::<u64>(), 7);
866    }
867
868    #[test]
869    fn callback_template_ctx_accessible() {
870        let mut builder = WorldBuilder::new();
871        builder.register::<u64>(0);
872        let mut world = builder.build();
873
874        let template = CallbackTemplate::<OnTimeout>::new(on_timeout, world.registry());
875        let mut cb = template.generate(TimerCtx {
876            order_id: 42,
877            fires: 0,
878        });
879        assert_eq!(cb.ctx().order_id, 42);
880        cb.run(&mut world, ());
881        assert_eq!(cb.ctx().fires, 1);
882        cb.ctx_mut().order_id = 99;
883        cb.run(&mut world, ());
884        assert_eq!(*world.resource::<u64>(), 42 + 99);
885    }
886
887    // -- Convenience macros ---------------------------------------------------
888
889    handler_blueprint!(MacroOnTick, Event = u32, Params = (ResMut<'static, u64>,));
890
891    #[test]
892    fn macro_handler_blueprint() {
893        let mut builder = WorldBuilder::new();
894        builder.register::<u64>(0);
895        let mut world = builder.build();
896
897        let template = HandlerTemplate::<MacroOnTick>::new(tick, world.registry());
898        let mut h = template.generate();
899        h.run(&mut world, 3);
900        assert_eq!(*world.resource::<u64>(), 3);
901    }
902
903    callback_blueprint!(MacroOnTimeout, Context = TimerCtx, Event = (), Params = (ResMut<'static, u64>,));
904
905    #[test]
906    fn macro_callback_blueprint() {
907        let mut builder = WorldBuilder::new();
908        builder.register::<u64>(0);
909        let mut world = builder.build();
910
911        let template = CallbackTemplate::<MacroOnTimeout>::new(on_timeout, world.registry());
912        let mut cb = template.generate(TimerCtx {
913            order_id: 5,
914            fires: 0,
915        });
916        cb.run(&mut world, ());
917        assert_eq!(cb.ctx().fires, 1);
918        assert_eq!(*world.resource::<u64>(), 5);
919    }
920}