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