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