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}