structecs/
handler.rs

1#[cfg(debug_assertions)]
2use crate::ExtractionMetadata;
3use crate::{Acquirable, Extractable, entity::EntityData};
4
5/// Metadata for debugging handler type information (debug builds only).
6#[cfg(debug_assertions)]
7struct HandlerMetadata {
8    base_type: &'static str,
9    concrete_type: &'static str,
10    signature: String,
11}
12
13/// Type-erased function wrapper that stores a handler function.
14struct TypeErasedFn<Args, Return> {
15    caller: Box<dyn Fn(EntityData, Args) -> Return + Send + Sync>,
16    #[cfg(debug_assertions)]
17    metadata: HandlerMetadata,
18}
19
20impl<Args, Return> TypeErasedFn<Args, Return> {
21    pub fn new<Base, Concrete>(
22        func: impl Fn(&Acquirable<Concrete>, Args) -> Return + Send + Sync + 'static,
23    ) -> Self
24    where
25        Base: Extractable,
26        Concrete: Extractable,
27    {
28        let caller = move |data: EntityData, args: Args| -> Return {
29            // SAFETY: Type relationship is validated in debug builds during ComponentHandler creation
30            #[allow(clippy::expect_used)]
31            let entity = data
32                .extract::<Concrete>()
33                .expect("Handler type mismatch - this is a bug in ComponentHandler");
34            func(&entity, args)
35        };
36
37        Self {
38            caller: Box::new(caller),
39            #[cfg(debug_assertions)]
40            metadata: HandlerMetadata {
41                base_type: std::any::type_name::<Base>(),
42                concrete_type: std::any::type_name::<Concrete>(),
43                signature: format!(
44                    "Fn(&Acquirable<{}>, {}) -> {}",
45                    std::any::type_name::<Concrete>(),
46                    std::any::type_name::<Args>(),
47                    std::any::type_name::<Return>()
48                ),
49            },
50        }
51    }
52
53    pub fn call<E: Extractable>(&self, entity: &Acquirable<E>, args: Args) -> Return {
54        (self.caller)(entity.inner.clone(), args)
55    }
56}
57
58// SAFETY: TypeErasedFn only contains a Box<dyn Fn> which is Send,
59// and the function itself is bounded by Send + Sync traits.
60unsafe impl<Args, Return> Send for TypeErasedFn<Args, Return> {}
61// SAFETY: TypeErasedFn only contains a Box<dyn Fn> which is Sync,
62// and the function itself is bounded by Send + Sync traits.
63unsafe impl<Args, Return> Sync for TypeErasedFn<Args, Return> {}
64
65/// A component handler that enables polymorphic behavior on entity hierarchies.
66///
67/// This handler allows you to define behavior for concrete entity types (like `Player` or `Zombie`)
68/// while storing the handler in a base type (like `Entity`). When querying for the base type,
69/// the handler will execute the concrete type's implementation.
70///
71/// # Type Parameters
72///
73/// - `Base`: The base struct type used for queries (e.g., `Entity`)
74/// - `Args`: The argument tuple type for the handler function (default: `()`)
75/// - `Return`: The return type of the handler function (default: `()`)
76///
77/// # Example
78///
79/// ```
80/// use structecs::*;
81///
82/// #[derive(Debug, Extractable)]
83/// pub struct Player {
84///     pub name: String,
85///     pub level: u32,
86/// }
87///
88/// let world = World::new();
89/// let player_id = world.add_entity(Player {
90///     name: "Hero".to_string(),
91///     level: 10,
92/// });
93///
94/// // Create a handler for Player entities
95/// let player_handler = ComponentHandler::<Player>::for_type::<Player>(|player, ()| {
96///     println!("Level {} player {} died!", player.level, player.name);
97/// });
98///
99/// // Query for Player and call handler
100/// for (id, player) in world.query::<Player>() {
101///     player_handler.call(&player, ());  // Calls Player-specific logic
102/// }
103/// ```
104pub struct ComponentHandler<Base: Extractable, Args = (), Return = ()> {
105    function: TypeErasedFn<Args, Return>,
106    _marker: std::marker::PhantomData<Base>,
107}
108
109impl<Base: Extractable, Args, Return> ComponentHandler<Base, Args, Return> {
110    /// Create a handler for entities of type `Concrete` that can be extracted as `Base`.
111    ///
112    /// # Type Parameters
113    ///
114    /// - `Concrete`: The actual entity type (e.g., `Player`, `Zombie`)
115    ///
116    /// The `Concrete` type must contain the `Base` type in its extraction metadata.
117    /// This is typically achieved using `#[extractable(field_name)]` on the concrete type.
118    ///
119    /// # Panics
120    ///
121    /// In debug builds, this function will panic if `Concrete` does not contain `Base`
122    /// in its extraction metadata. This helps catch type mismatches early during development.
123    ///
124    /// # Example
125    ///
126    /// ```
127    /// use structecs::*;
128    ///
129    /// #[derive(Debug, Extractable)]
130    /// pub struct Player {
131    ///     pub name: String,
132    ///     pub level: u32,
133    /// }
134    ///
135    /// // Handler for Player entities
136    /// let handler = ComponentHandler::<Player>::for_type::<Player>(|player, ()| {
137    ///     println!("Player {} died", player.name);
138    /// });
139    /// ```
140    pub fn for_type<Concrete: Extractable>(
141        func: impl Fn(&Acquirable<Concrete>, Args) -> Return + Send + Sync + 'static,
142    ) -> Self {
143        #[cfg(debug_assertions)]
144        Self::validate_type_relationship::<Concrete>();
145
146        Self {
147            function: TypeErasedFn::new::<Base, Concrete>(func),
148            _marker: std::marker::PhantomData,
149        }
150    }
151
152    /// Validate that Concrete can be extracted as Base (debug builds only).
153    #[cfg(debug_assertions)]
154    fn validate_type_relationship<Concrete: Extractable>() {
155        if !can_extract::<Concrete, Base>() {
156            panic!(
157                "\n╔════════════════════════════════════════════════════════════╗\n\
158                 ║ ComponentHandler Type Mismatch                             ║\n\
159                 ╠════════════════════════════════════════════════════════════╣\n\
160                 ║ Base type:     {:<44}║\n\
161                 ║ Concrete type: {:<44}║\n\
162                 ╠════════════════════════════════════════════════════════════╣\n\
163                 ║ The concrete type must contain the base type in its        ║\n\
164                 ║ extraction metadata. Did you forget #[extractable(...)]?   ║\n\
165                 ║                                                            ║\n\
166                 ║ Example:                                                   ║\n\
167                 ║   #[derive(Extractable)]                                   ║\n\
168                 ║   #[extractable(entity)]  // <-- Add this!                 ║\n\
169                 ║   pub struct Player {{                                      ║\n\
170                 ║       pub entity: Entity,                                  ║\n\
171                 ║   }}                                                        ║\n\
172                 ╚════════════════════════════════════════════════════════════╝\n",
173                std::any::type_name::<Base>(),
174                std::any::type_name::<Concrete>()
175            );
176        }
177    }
178
179    /// Call the handler with an entity.
180    ///
181    /// # Type Parameters
182    ///
183    /// - `E`: The entity type being passed to the handler
184    ///
185    /// The entity type `E` must be extractable as `Base`. The handler will then
186    /// extract the concrete type it was created with and call the appropriate function.
187    ///
188    /// # Panics
189    ///
190    /// In debug builds, this function will panic if `E` cannot be extracted as `Base`.
191    ///
192    /// # Example
193    ///
194    /// ```
195    /// use structecs::*;
196    ///
197    /// #[derive(Debug, Extractable)]
198    /// pub struct Player {
199    ///     pub name: String,
200    ///     pub level: u32,
201    /// }
202    ///
203    /// let world = World::new();
204    /// let player_id = world.add_entity(Player {
205    ///     name: "Hero".to_string(),
206    ///     level: 10,
207    /// });
208    ///
209    /// let handler = ComponentHandler::<Player>::for_type::<Player>(|player, ()| {
210    ///     println!("Player died");
211    /// });
212    ///
213    /// for (id, player) in world.query::<Player>() {
214    ///     handler.call(&player, ());
215    /// }
216    /// ```
217    pub fn call<E: Extractable>(&self, entity: &Acquirable<E>, args: Args) -> Return {
218        #[cfg(debug_assertions)]
219        self.validate_call::<E>();
220
221        self.function.call(entity, args)
222    }
223
224    /// Validate that the entity type can be extracted as Base (debug builds only).
225    #[cfg(debug_assertions)]
226    fn validate_call<E: Extractable>(&self) {
227        if !can_extract::<E, Base>() {
228            panic!(
229                "\n╔════════════════════════════════════════════════════════════╗\n\
230                 ║ ComponentHandler Call Mismatch                             ║\n\
231                 ╠════════════════════════════════════════════════════════════╣\n\
232                 ║ Expected base:  {:<44}║\n\
233                 ║ Actual type:    {:<44}║\n\
234                 ║ Handler for:    {:<44}║\n\
235                 ╠════════════════════════════════════════════════════════════╣\n\
236                 ║ The entity type must be extractable as the base type.      ║\n\
237                 ╚════════════════════════════════════════════════════════════╝\n",
238                std::any::type_name::<Base>(),
239                std::any::type_name::<E>(),
240                self.function.metadata.concrete_type
241            );
242        }
243    }
244
245    /// Get debug information about this handler (debug builds only).
246    ///
247    /// Returns a string containing the base type, concrete type, and function signature.
248    ///
249    /// # Example
250    ///
251    /// ```
252    /// use structecs::*;
253    ///
254    /// #[derive(Debug, Extractable)]
255    /// pub struct Player {
256    ///     pub name: String,
257    ///     pub level: u32,
258    /// }
259    ///
260    /// let handler = ComponentHandler::<Player>::for_type::<Player>(|player, ()| {
261    ///     println!("Player died");
262    /// });
263    ///
264    /// #[cfg(debug_assertions)]
265    /// {
266    ///     let info = handler.debug_info();
267    ///     assert!(info.contains("ComponentHandler"));
268    ///     assert!(info.contains("Player"));
269    /// }
270    /// ```
271    #[cfg(debug_assertions)]
272    pub fn debug_info(&self) -> String {
273        format!(
274            "ComponentHandler<{}> for {} (signature: {})",
275            self.function.metadata.base_type,
276            self.function.metadata.concrete_type,
277            self.function.metadata.signature
278        )
279    }
280}
281
282impl<Base: Extractable, Args, Return> std::fmt::Debug for ComponentHandler<Base, Args, Return> {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        let mut debug = f.debug_struct("ComponentHandler");
285        #[cfg(debug_assertions)]
286        {
287            debug.field("base_type", &self.function.metadata.base_type);
288            debug.field("concrete_type", &self.function.metadata.concrete_type);
289            debug.field("signature", &self.function.metadata.signature);
290        }
291        #[cfg(not(debug_assertions))]
292        debug.field("function", &"<type erased>");
293        debug.finish()
294    }
295}
296
297/// Helper function to search for a target type in extraction metadata.
298#[cfg(debug_assertions)]
299fn search_metadata(list: &[ExtractionMetadata], target: std::any::TypeId) -> bool {
300    for metadata in list {
301        match metadata {
302            ExtractionMetadata::Target { type_id, .. } => {
303                if *type_id == target {
304                    return true;
305                }
306            }
307            ExtractionMetadata::Nested {
308                type_id, nested, ..
309            } => {
310                if *type_id == target || search_metadata(nested, target) {
311                    return true;
312                }
313            }
314        }
315    }
316    false
317}
318
319/// Check if `Concrete` can be extracted as `Base`.
320///
321/// Returns `true` if the `Base` type exists in `Concrete`'s extraction metadata.
322///
323/// Note: [`std::any::TypeId`] is not const evaluable yet, so this can't be a const fn.
324#[cfg(debug_assertions)]
325fn can_extract<Concrete: Extractable, Base: Extractable>() -> bool {
326    let base_type_id = std::any::TypeId::of::<Base>();
327    search_metadata(Concrete::METADATA_LIST, base_type_id)
328}