magi_codex/
map.rs

1use dashmap::DashMap;
2use std::any::TypeId;
3use std::hash::Hash;
4use std::sync::Arc;
5
6/// Type-indexed map storage - multiple values per type, indexed by keys.
7///
8/// `CodexMap` stores multiple values of type `V` indexed by keys of type `K`,
9/// similar to how a `HashMap` stores key-value pairs, but with type-level isolation
10/// where different `(K, V)` type pairs are stored separately.
11///
12/// Useful for sessions, caches, per-user data, etc.
13///
14/// ## Cloning
15///
16/// `CodexMap` is cheaply cloneable via `Arc`. All clones share the same storage:
17///
18/// ```
19/// use magi_codex::CodexMap;
20///
21/// let map = CodexMap::new();
22/// map.insert("user_1", 100u32);
23///
24/// let map2 = map.clone();
25/// assert_eq!(map2.get_cloned::<&str, u32>(&"user_1"), Some(100u32));
26/// ```
27#[derive(Clone)]
28pub struct CodexMap {
29    inner: Arc<DashMap<TypeId, Box<dyn std::any::Any + Send + Sync>>>,
30}
31
32impl Default for CodexMap {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl CodexMap {
39    /// Create a new empty map.
40    #[inline]
41    pub fn new() -> Self {
42        Self {
43            inner: Arc::new(DashMap::new()),
44        }
45    }
46
47    /// Insert or replace a key-value pair.
48    ///
49    /// # Example
50    /// ```
51    /// use magi_codex::CodexMap;
52    ///
53    /// #[derive(Clone)]
54    /// struct Session { user_id: String }
55    ///
56    /// let map = CodexMap::new();
57    /// map.insert("user_123", Session { user_id: "123".into() });
58    /// ```
59    pub fn insert<K, V>(&self, key: K, value: V)
60    where
61        K: Hash + Eq + Send + Sync + 'static,
62        V: Send + Sync + 'static,
63    {
64        let type_id = TypeId::of::<(K, V)>();
65
66        if !self.inner.contains_key(&type_id) {
67            self.inner.insert(
68                type_id,
69                Box::new(DashMap::<K, V>::new()) as Box<dyn std::any::Any + Send + Sync>,
70            );
71        }
72
73        let storage = self.inner.get(&type_id).expect("just inserted");
74        let map = storage
75            .downcast_ref::<DashMap<K, V>>()
76            .expect("type mismatch in keyed storage");
77        map.insert(key, value);
78    }
79
80    /// Access a value immutably via a closure.
81    ///
82    /// The closure runs while holding the lock on the value.
83    pub fn with<K, V, F, R>(&self, key: &K, f: F) -> Option<R>
84    where
85        K: Hash + Eq + Send + Sync + 'static,
86        V: Send + Sync + 'static,
87        F: FnOnce(&V) -> R,
88    {
89        let type_id = TypeId::of::<(K, V)>();
90        let storage = self.inner.get(&type_id)?;
91        let map = storage.downcast_ref::<DashMap<K, V>>()?;
92        let value_ref = map.get(key)?;
93        Some(f(value_ref.value()))
94    }
95
96    /// Access a value mutably via a closure.
97    ///
98    /// The closure runs while holding the lock on the value.
99    pub fn with_mut<K, V, F, R>(&self, key: &K, f: F) -> Option<R>
100    where
101        K: Hash + Eq + Send + Sync + 'static,
102        V: Send + Sync + 'static,
103        F: FnOnce(&mut V) -> R,
104    {
105        let type_id = TypeId::of::<(K, V)>();
106        let storage = self.inner.get(&type_id)?;
107        let map = storage.downcast_ref::<DashMap<K, V>>()?;
108        let mut value_ref = map.get_mut(key)?;
109        Some(f(value_ref.value_mut()))
110    }
111
112    /// Clone and return a value (requires Clone).
113    pub fn get_cloned<K, V>(&self, key: &K) -> Option<V>
114    where
115        K: Hash + Eq + Send + Sync + 'static,
116        V: Clone + Send + Sync + 'static,
117    {
118        self.with::<K, V, _, _>(key, |v: &V| v.clone())
119    }
120
121    /// Remove and return a key-value pair.
122    pub fn remove<K, V>(&self, key: &K) -> Option<(K, V)>
123    where
124        K: Hash + Eq + Send + Sync + 'static,
125        V: Send + Sync + 'static,
126    {
127        let type_id = TypeId::of::<(K, V)>();
128        let storage = self.inner.get(&type_id)?;
129        let map = storage.downcast_ref::<DashMap<K, V>>()?;
130        map.remove(key)
131    }
132
133    /// Returns true if a key exists.
134    pub fn contains<K, V>(&self, key: &K) -> bool
135    where
136        K: Hash + Eq + Send + Sync + 'static,
137        V: Send + Sync + 'static,
138    {
139        let type_id = TypeId::of::<(K, V)>();
140        if let Some(storage) = self.inner.get(&type_id)
141            && let Some(map) = storage.downcast_ref::<DashMap<K, V>>()
142        {
143            return map.contains_key(key);
144        }
145        false
146    }
147
148    /// Iterate over all key-value pairs via a closure.
149    pub fn for_each<K, V, F>(&self, mut f: F)
150    where
151        K: Hash + Eq + Send + Sync + 'static,
152        V: Send + Sync + 'static,
153        F: FnMut(&K, &V),
154    {
155        let type_id = TypeId::of::<(K, V)>();
156        if let Some(storage) = self.inner.get(&type_id)
157            && let Some(map) = storage.downcast_ref::<DashMap<K, V>>()
158        {
159            for entry in map.iter() {
160                let (key, value) = entry.pair();
161                f(key, value);
162            }
163        }
164    }
165
166    /// Returns the number of entries for a given type pair.
167    pub fn len<K, V>(&self) -> usize
168    where
169        K: Hash + Eq + Send + Sync + 'static,
170        V: Send + Sync + 'static,
171    {
172        let type_id = TypeId::of::<(K, V)>();
173        if let Some(storage) = self.inner.get(&type_id)
174            && let Some(map) = storage.downcast_ref::<DashMap<K, V>>()
175        {
176            return map.len();
177        }
178        0
179    }
180
181    /// Returns true if there are no values of the given type.
182    pub fn is_empty<K, V>(&self) -> bool
183    where
184        K: Hash + Eq + Send + Sync + 'static,
185        V: Send + Sync + 'static,
186    {
187        self.len::<K, V>() == 0
188    }
189
190    // ========================================================================
191    // Composition Queries - Access multiple value types for the same key
192    // ========================================================================
193
194    /// Check if a key has values of two types.
195    pub fn contains2<K, V1, V2>(&self, key: &K) -> bool
196    where
197        K: Hash + Eq + Send + Sync + 'static,
198        V1: Send + Sync + 'static,
199        V2: Send + Sync + 'static,
200    {
201        self.contains::<K, V1>(key) && self.contains::<K, V2>(key)
202    }
203
204    /// Check if a key has values of three types.
205    pub fn contains3<K, V1, V2, V3>(&self, key: &K) -> bool
206    where
207        K: Hash + Eq + Send + Sync + 'static,
208        V1: Send + Sync + 'static,
209        V2: Send + Sync + 'static,
210        V3: Send + Sync + 'static,
211    {
212        self.contains::<K, V1>(key) && self.contains::<K, V2>(key) && self.contains::<K, V3>(key)
213    }
214
215    /// Check if a key has values of four types.
216    pub fn contains4<K, V1, V2, V3, V4>(&self, key: &K) -> bool
217    where
218        K: Hash + Eq + Send + Sync + 'static,
219        V1: Send + Sync + 'static,
220        V2: Send + Sync + 'static,
221        V3: Send + Sync + 'static,
222        V4: Send + Sync + 'static,
223    {
224        self.contains::<K, V1>(key)
225            && self.contains::<K, V2>(key)
226            && self.contains::<K, V3>(key)
227            && self.contains::<K, V4>(key)
228    }
229
230    /// Access two value types for the same key via a closure.
231    pub fn with2<K, V1, V2, F, R>(&self, key: &K, f: F) -> Option<R>
232    where
233        K: Hash + Eq + Send + Sync + 'static,
234        V1: Send + Sync + 'static,
235        V2: Send + Sync + 'static,
236        F: FnOnce(&V1, &V2) -> R,
237    {
238        let type_id1 = TypeId::of::<(K, V1)>();
239        let type_id2 = TypeId::of::<(K, V2)>();
240
241        let storage1 = self.inner.get(&type_id1)?;
242        let storage2 = self.inner.get(&type_id2)?;
243
244        let map1 = storage1.downcast_ref::<DashMap<K, V1>>()?;
245        let map2 = storage2.downcast_ref::<DashMap<K, V2>>()?;
246
247        let value1 = map1.get(key)?;
248        let value2 = map2.get(key)?;
249
250        Some(f(value1.value(), value2.value()))
251    }
252
253    /// Access three value types for the same key via a closure.
254    pub fn with3<K, V1, V2, V3, F, R>(&self, key: &K, f: F) -> Option<R>
255    where
256        K: Hash + Eq + Send + Sync + 'static,
257        V1: Send + Sync + 'static,
258        V2: Send + Sync + 'static,
259        V3: Send + Sync + 'static,
260        F: FnOnce(&V1, &V2, &V3) -> R,
261    {
262        let type_id1 = TypeId::of::<(K, V1)>();
263        let type_id2 = TypeId::of::<(K, V2)>();
264        let type_id3 = TypeId::of::<(K, V3)>();
265
266        let storage1 = self.inner.get(&type_id1)?;
267        let storage2 = self.inner.get(&type_id2)?;
268        let storage3 = self.inner.get(&type_id3)?;
269
270        let map1 = storage1.downcast_ref::<DashMap<K, V1>>()?;
271        let map2 = storage2.downcast_ref::<DashMap<K, V2>>()?;
272        let map3 = storage3.downcast_ref::<DashMap<K, V3>>()?;
273
274        let value1 = map1.get(key)?;
275        let value2 = map2.get(key)?;
276        let value3 = map3.get(key)?;
277
278        Some(f(value1.value(), value2.value(), value3.value()))
279    }
280
281    /// Access four value types for the same key via a closure.
282    pub fn with4<K, V1, V2, V3, V4, F, R>(&self, key: &K, f: F) -> Option<R>
283    where
284        K: Hash + Eq + Send + Sync + 'static,
285        V1: Send + Sync + 'static,
286        V2: Send + Sync + 'static,
287        V3: Send + Sync + 'static,
288        V4: Send + Sync + 'static,
289        F: FnOnce(&V1, &V2, &V3, &V4) -> R,
290    {
291        let type_id1 = TypeId::of::<(K, V1)>();
292        let type_id2 = TypeId::of::<(K, V2)>();
293        let type_id3 = TypeId::of::<(K, V3)>();
294        let type_id4 = TypeId::of::<(K, V4)>();
295
296        let storage1 = self.inner.get(&type_id1)?;
297        let storage2 = self.inner.get(&type_id2)?;
298        let storage3 = self.inner.get(&type_id3)?;
299        let storage4 = self.inner.get(&type_id4)?;
300
301        let map1 = storage1.downcast_ref::<DashMap<K, V1>>()?;
302        let map2 = storage2.downcast_ref::<DashMap<K, V2>>()?;
303        let map3 = storage3.downcast_ref::<DashMap<K, V3>>()?;
304        let map4 = storage4.downcast_ref::<DashMap<K, V4>>()?;
305
306        let value1 = map1.get(key)?;
307        let value2 = map2.get(key)?;
308        let value3 = map3.get(key)?;
309        let value4 = map4.get(key)?;
310
311        Some(f(
312            value1.value(),
313            value2.value(),
314            value3.value(),
315            value4.value(),
316        ))
317    }
318
319    /// Mutably access two value types for the same key via a closure.
320    pub fn with2_mut<K, V1, V2, F, R>(&self, key: &K, f: F) -> Option<R>
321    where
322        K: Hash + Eq + Send + Sync + 'static,
323        V1: Send + Sync + 'static,
324        V2: Send + Sync + 'static,
325        F: FnOnce(&mut V1, &mut V2) -> R,
326    {
327        let type_id1 = TypeId::of::<(K, V1)>();
328        let type_id2 = TypeId::of::<(K, V2)>();
329
330        let storage1 = self.inner.get(&type_id1)?;
331        let storage2 = self.inner.get(&type_id2)?;
332
333        let map1 = storage1.downcast_ref::<DashMap<K, V1>>()?;
334        let map2 = storage2.downcast_ref::<DashMap<K, V2>>()?;
335
336        let mut value1 = map1.get_mut(key)?;
337        let mut value2 = map2.get_mut(key)?;
338
339        Some(f(value1.value_mut(), value2.value_mut()))
340    }
341
342    /// Mutably access three value types for the same key via a closure.
343    pub fn with3_mut<K, V1, V2, V3, F, R>(&self, key: &K, f: F) -> Option<R>
344    where
345        K: Hash + Eq + Send + Sync + 'static,
346        V1: Send + Sync + 'static,
347        V2: Send + Sync + 'static,
348        V3: Send + Sync + 'static,
349        F: FnOnce(&mut V1, &mut V2, &mut V3) -> R,
350    {
351        let type_id1 = TypeId::of::<(K, V1)>();
352        let type_id2 = TypeId::of::<(K, V2)>();
353        let type_id3 = TypeId::of::<(K, V3)>();
354
355        let storage1 = self.inner.get(&type_id1)?;
356        let storage2 = self.inner.get(&type_id2)?;
357        let storage3 = self.inner.get(&type_id3)?;
358
359        let map1 = storage1.downcast_ref::<DashMap<K, V1>>()?;
360        let map2 = storage2.downcast_ref::<DashMap<K, V2>>()?;
361        let map3 = storage3.downcast_ref::<DashMap<K, V3>>()?;
362
363        let mut value1 = map1.get_mut(key)?;
364        let mut value2 = map2.get_mut(key)?;
365        let mut value3 = map3.get_mut(key)?;
366
367        Some(f(
368            value1.value_mut(),
369            value2.value_mut(),
370            value3.value_mut(),
371        ))
372    }
373
374    /// Mutably access four value types for the same key via a closure.
375    pub fn with4_mut<K, V1, V2, V3, V4, F, R>(&self, key: &K, f: F) -> Option<R>
376    where
377        K: Hash + Eq + Send + Sync + 'static,
378        V1: Send + Sync + 'static,
379        V2: Send + Sync + 'static,
380        V3: Send + Sync + 'static,
381        V4: Send + Sync + 'static,
382        F: FnOnce(&mut V1, &mut V2, &mut V3, &mut V4) -> R,
383    {
384        let type_id1 = TypeId::of::<(K, V1)>();
385        let type_id2 = TypeId::of::<(K, V2)>();
386        let type_id3 = TypeId::of::<(K, V3)>();
387        let type_id4 = TypeId::of::<(K, V4)>();
388
389        let storage1 = self.inner.get(&type_id1)?;
390        let storage2 = self.inner.get(&type_id2)?;
391        let storage3 = self.inner.get(&type_id3)?;
392        let storage4 = self.inner.get(&type_id4)?;
393
394        let map1 = storage1.downcast_ref::<DashMap<K, V1>>()?;
395        let map2 = storage2.downcast_ref::<DashMap<K, V2>>()?;
396        let map3 = storage3.downcast_ref::<DashMap<K, V3>>()?;
397        let map4 = storage4.downcast_ref::<DashMap<K, V4>>()?;
398
399        let mut value1 = map1.get_mut(key)?;
400        let mut value2 = map2.get_mut(key)?;
401        let mut value3 = map3.get_mut(key)?;
402        let mut value4 = map4.get_mut(key)?;
403
404        Some(f(
405            value1.value_mut(),
406            value2.value_mut(),
407            value3.value_mut(),
408            value4.value_mut(),
409        ))
410    }
411
412    // ========================================================================
413    // Get Cloned Methods - Return owned copies of multiple values
414    // ========================================================================
415
416    /// Clone and return two value types for the same key.
417    pub fn get2_cloned<K, V1, V2>(&self, key: &K) -> Option<(V1, V2)>
418    where
419        K: Hash + Eq + Send + Sync + 'static,
420        V1: Clone + Send + Sync + 'static,
421        V2: Clone + Send + Sync + 'static,
422    {
423        self.with2::<K, V1, V2, _, _>(key, |v1, v2| (v1.clone(), v2.clone()))
424    }
425
426    /// Clone and return three value types for the same key.
427    pub fn get3_cloned<K, V1, V2, V3>(&self, key: &K) -> Option<(V1, V2, V3)>
428    where
429        K: Hash + Eq + Send + Sync + 'static,
430        V1: Clone + Send + Sync + 'static,
431        V2: Clone + Send + Sync + 'static,
432        V3: Clone + Send + Sync + 'static,
433    {
434        self.with3::<K, V1, V2, V3, _, _>(key, |v1, v2, v3| (v1.clone(), v2.clone(), v3.clone()))
435    }
436
437    /// Clone and return four value types for the same key.
438    pub fn get4_cloned<K, V1, V2, V3, V4>(&self, key: &K) -> Option<(V1, V2, V3, V4)>
439    where
440        K: Hash + Eq + Send + Sync + 'static,
441        V1: Clone + Send + Sync + 'static,
442        V2: Clone + Send + Sync + 'static,
443        V3: Clone + Send + Sync + 'static,
444        V4: Clone + Send + Sync + 'static,
445    {
446        self.with4::<K, V1, V2, V3, V4, _, _>(key, |v1, v2, v3, v4| {
447            (v1.clone(), v2.clone(), v3.clone(), v4.clone())
448        })
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[derive(Clone, Debug, PartialEq)]
457    struct Session {
458        user_id: String,
459    }
460
461    #[test]
462    fn basic_operations() {
463        let map = CodexMap::new();
464
465        map.insert(
466            "user_1",
467            Session {
468                user_id: "1".into(),
469            },
470        );
471        map.insert(
472            "user_2",
473            Session {
474                user_id: "2".into(),
475            },
476        );
477
478        assert!(map.contains::<&str, Session>(&"user_1"));
479        assert_eq!(map.len::<&str, Session>(), 2);
480
481        let user_id = map
482            .with::<&str, Session, _, _>(&"user_1", |session| session.user_id.clone())
483            .unwrap();
484        assert_eq!(user_id, "1");
485    }
486
487    #[test]
488    fn with_mut() {
489        let map = CodexMap::new();
490        map.insert(
491            "user_1",
492            Session {
493                user_id: "1".into(),
494            },
495        );
496
497        map.with_mut::<&str, Session, _, _>(&"user_1", |session| {
498            session.user_id = "updated".into();
499        });
500
501        let user_id = map
502            .with::<&str, Session, _, _>(&"user_1", |session| session.user_id.clone())
503            .unwrap();
504        assert_eq!(user_id, "updated");
505    }
506
507    #[test]
508    fn remove() {
509        let map = CodexMap::new();
510        map.insert(
511            "user_1",
512            Session {
513                user_id: "1".into(),
514            },
515        );
516
517        let (key, session) = map.remove::<&str, Session>(&"user_1").unwrap();
518        assert_eq!(key, "user_1");
519        assert_eq!(session.user_id, "1");
520        assert!(!map.contains::<&str, Session>(&"user_1"));
521    }
522
523    #[test]
524    fn for_each() {
525        let map = CodexMap::new();
526        map.insert(
527            "user_1",
528            Session {
529                user_id: "1".into(),
530            },
531        );
532        map.insert(
533            "user_2",
534            Session {
535                user_id: "2".into(),
536            },
537        );
538
539        let mut count = 0;
540        map.for_each::<&str, Session, _>(|_key, session| {
541            assert!(session.user_id == "1" || session.user_id == "2");
542            count += 1;
543        });
544        assert_eq!(count, 2);
545    }
546
547    #[test]
548    fn type_isolation() {
549        let map = CodexMap::new();
550
551        #[derive(Clone)]
552        struct Config {
553            debug: bool,
554        }
555
556        map.insert(
557            "key1",
558            Session {
559                user_id: "1".into(),
560            },
561        );
562        map.insert("key1", Config { debug: true });
563
564        assert!(map.contains::<&str, Session>(&"key1"));
565        assert!(map.contains::<&str, Config>(&"key1"));
566    }
567
568    #[test]
569    fn clone_shares_storage() {
570        let map = CodexMap::new();
571        map.insert(
572            "user_1",
573            Session {
574                user_id: "1".into(),
575            },
576        );
577
578        let map2 = map.clone();
579
580        assert!(map2.contains::<&str, Session>(&"user_1"));
581
582        map2.insert(
583            "user_2",
584            Session {
585                user_id: "2".into(),
586            },
587        );
588
589        assert!(map.contains::<&str, Session>(&"user_2"));
590        assert_eq!(map.len::<&str, Session>(), 2);
591    }
592
593    #[tokio::test]
594    async fn clone_across_tasks() {
595        let map = CodexMap::new();
596        map.insert("initial", 42u32);
597
598        let map_clone = map.clone();
599
600        let handle = tokio::spawn(async move {
601            map_clone.insert("from_task", 100u32);
602            map_clone.get_cloned::<&str, u32>(&"initial")
603        });
604
605        let value = handle.await.unwrap();
606        assert_eq!(value, Some(42));
607
608        assert_eq!(map.get_cloned::<&str, u32>(&"from_task"), Some(100));
609    }
610
611    // ========================================================================
612    // Composition Query Tests - ECS-like component queries
613    // ========================================================================
614
615    #[derive(Clone, Debug, PartialEq)]
616    struct Position {
617        x: f32,
618        y: f32,
619    }
620
621    #[derive(Clone, Debug, PartialEq)]
622    struct Velocity {
623        dx: f32,
624        dy: f32,
625    }
626
627    #[derive(Clone, Debug, PartialEq)]
628    struct Health {
629        hp: i32,
630    }
631
632    #[derive(Clone, Debug, PartialEq)]
633    struct Name {
634        value: String,
635    }
636
637    #[test]
638    fn composition_contains2() {
639        let map = CodexMap::new();
640
641        map.insert("entity_1", Position { x: 0.0, y: 0.0 });
642        map.insert("entity_1", Velocity { dx: 1.0, dy: 0.5 });
643        map.insert("entity_2", Position { x: 5.0, y: 3.0 });
644
645        // entity_1 has both Position and Velocity
646        assert!(map.contains2::<&str, Position, Velocity>(&"entity_1"));
647
648        // entity_2 has Position but not Velocity
649        assert!(!map.contains2::<&str, Position, Velocity>(&"entity_2"));
650
651        // entity_3 doesn't exist
652        assert!(!map.contains2::<&str, Position, Velocity>(&"entity_3"));
653    }
654
655    #[test]
656    fn composition_contains3() {
657        let map = CodexMap::new();
658
659        map.insert("entity_1", Position { x: 0.0, y: 0.0 });
660        map.insert("entity_1", Velocity { dx: 1.0, dy: 0.5 });
661        map.insert("entity_1", Health { hp: 100 });
662
663        assert!(map.contains3::<&str, Position, Velocity, Health>(&"entity_1"));
664
665        map.insert("entity_2", Position { x: 5.0, y: 3.0 });
666        map.insert("entity_2", Velocity { dx: -1.0, dy: 2.0 });
667
668        // entity_2 missing Health
669        assert!(!map.contains3::<&str, Position, Velocity, Health>(&"entity_2"));
670    }
671
672    #[test]
673    fn composition_with2() {
674        let map = CodexMap::new();
675
676        map.insert("player", Position { x: 0.0, y: 0.0 });
677        map.insert("player", Velocity { dx: 1.0, dy: 0.5 });
678
679        // Query both components together
680        let result = map
681            .with2::<&str, Position, Velocity, _, _>(&"player", |pos, vel| {
682                format!("at ({}, {}) moving ({}, {})", pos.x, pos.y, vel.dx, vel.dy)
683            })
684            .unwrap();
685
686        assert_eq!(result, "at (0, 0) moving (1, 0.5)");
687    }
688
689    #[test]
690    fn composition_with2_mut() {
691        let map = CodexMap::new();
692
693        map.insert("player", Position { x: 0.0, y: 0.0 });
694        map.insert("player", Velocity { dx: 1.0, dy: 0.5 });
695
696        // Update position based on velocity
697        map.with2_mut::<&str, Position, Velocity, _, _>(&"player", |pos, vel| {
698            pos.x += vel.dx;
699            pos.y += vel.dy;
700        });
701
702        let pos = map.get_cloned::<&str, Position>(&"player").unwrap();
703        assert_eq!(pos, Position { x: 1.0, y: 0.5 });
704    }
705
706    #[test]
707    fn composition_with3() {
708        let map = CodexMap::new();
709
710        map.insert("player", Position { x: 10.0, y: 20.0 });
711        map.insert("player", Velocity { dx: 1.0, dy: 2.0 });
712        map.insert("player", Health { hp: 100 });
713
714        let result = map
715            .with3::<&str, Position, Velocity, Health, _, _>(&"player", |pos, vel, health| {
716                format!(
717                    "Player at ({}, {}) moving ({}, {}) with {} HP",
718                    pos.x, pos.y, vel.dx, vel.dy, health.hp
719                )
720            })
721            .unwrap();
722
723        assert_eq!(result, "Player at (10, 20) moving (1, 2) with 100 HP");
724    }
725
726    #[test]
727    fn composition_with3_mut() {
728        let map = CodexMap::new();
729
730        map.insert("player", Position { x: 0.0, y: 0.0 });
731        map.insert("player", Velocity { dx: 2.0, dy: 3.0 });
732        map.insert("player", Health { hp: 100 });
733
734        // Update multiple components at once
735        map.with3_mut::<&str, Position, Velocity, Health, _, _>(&"player", |pos, vel, health| {
736            pos.x += vel.dx;
737            pos.y += vel.dy;
738            vel.dx *= 0.9; // Friction
739            vel.dy *= 0.9;
740            health.hp -= 1; // Damage over time
741        });
742
743        let pos = map.get_cloned::<&str, Position>(&"player").unwrap();
744        let vel = map.get_cloned::<&str, Velocity>(&"player").unwrap();
745        let health = map.get_cloned::<&str, Health>(&"player").unwrap();
746
747        assert_eq!(pos, Position { x: 2.0, y: 3.0 });
748        assert!((vel.dx - 1.8).abs() < 0.001);
749        assert!((vel.dy - 2.7).abs() < 0.001);
750        assert_eq!(health.hp, 99);
751    }
752
753    #[test]
754    fn composition_with4() {
755        let map = CodexMap::new();
756
757        map.insert(
758            "player",
759            Name {
760                value: "Hero".to_string(),
761            },
762        );
763        map.insert("player", Position { x: 10.0, y: 20.0 });
764        map.insert("player", Velocity { dx: 1.0, dy: 2.0 });
765        map.insert("player", Health { hp: 100 });
766
767        let result = map
768            .with4::<&str, Name, Position, Velocity, Health, _, _>(
769                &"player",
770                |name, pos, vel, health| {
771                    format!(
772                        "{} at ({}, {}) moving ({}, {}) with {} HP",
773                        name.value, pos.x, pos.y, vel.dx, vel.dy, health.hp
774                    )
775                },
776            )
777            .unwrap();
778
779        assert_eq!(result, "Hero at (10, 20) moving (1, 2) with 100 HP");
780    }
781
782    #[test]
783    fn composition_get2_cloned() {
784        let map = CodexMap::new();
785
786        map.insert("player", Position { x: 5.0, y: 10.0 });
787        map.insert("player", Velocity { dx: 2.0, dy: 3.0 });
788
789        // Get cloned copies of both components
790        let (pos, vel) = map
791            .get2_cloned::<&str, Position, Velocity>(&"player")
792            .unwrap();
793
794        assert_eq!(pos, Position { x: 5.0, y: 10.0 });
795        assert_eq!(vel, Velocity { dx: 2.0, dy: 3.0 });
796
797        // Original values still exist in map
798        assert!(map.contains::<&str, Position>(&"player"));
799        assert!(map.contains::<&str, Velocity>(&"player"));
800    }
801
802    #[test]
803    fn composition_get3_cloned() {
804        let map = CodexMap::new();
805
806        map.insert("player", Position { x: 5.0, y: 10.0 });
807        map.insert("player", Velocity { dx: 2.0, dy: 3.0 });
808        map.insert("player", Health { hp: 100 });
809
810        let (pos, vel, health) = map
811            .get3_cloned::<&str, Position, Velocity, Health>(&"player")
812            .unwrap();
813
814        assert_eq!(pos, Position { x: 5.0, y: 10.0 });
815        assert_eq!(vel, Velocity { dx: 2.0, dy: 3.0 });
816        assert_eq!(health, Health { hp: 100 });
817    }
818
819    #[test]
820    fn composition_get4_cloned() {
821        let map = CodexMap::new();
822
823        map.insert(
824            "player",
825            Name {
826                value: "Hero".to_string(),
827            },
828        );
829        map.insert("player", Position { x: 5.0, y: 10.0 });
830        map.insert("player", Velocity { dx: 2.0, dy: 3.0 });
831        map.insert("player", Health { hp: 100 });
832
833        let (name, pos, vel, health) = map
834            .get4_cloned::<&str, Name, Position, Velocity, Health>(&"player")
835            .unwrap();
836
837        assert_eq!(
838            name,
839            Name {
840                value: "Hero".to_string()
841            }
842        );
843        assert_eq!(pos, Position { x: 5.0, y: 10.0 });
844        assert_eq!(vel, Velocity { dx: 2.0, dy: 3.0 });
845        assert_eq!(health, Health { hp: 100 });
846    }
847
848    #[test]
849    fn composition_get_cloned_can_be_moved() {
850        let map = CodexMap::new();
851
852        map.insert("player", Position { x: 5.0, y: 10.0 });
853        map.insert("player", Velocity { dx: 2.0, dy: 3.0 });
854
855        // Get owned copies - can be moved around freely
856        let (pos, vel) = map
857            .get2_cloned::<&str, Position, Velocity>(&"player")
858            .unwrap();
859
860        // Can pass to a function that takes ownership
861        fn process_components(p: Position, v: Velocity) -> f32 {
862            p.x + v.dx
863        }
864
865        let result = process_components(pos, vel);
866        assert_eq!(result, 7.0);
867    }
868
869    #[test]
870    fn composition_missing_component_returns_none() {
871        let map = CodexMap::new();
872
873        map.insert("entity", Position { x: 0.0, y: 0.0 });
874        // Missing Velocity
875
876        // with2 returns None when component is missing
877        let result = map.with2::<&str, Position, Velocity, _, _>(&"entity", |_, _| ());
878        assert!(result.is_none());
879
880        // get2_cloned also returns None when component is missing
881        let result = map.get2_cloned::<&str, Position, Velocity>(&"entity");
882        assert!(result.is_none());
883    }
884
885    #[test]
886    fn composition_multiple_entities() {
887        let map = CodexMap::new();
888
889        // Entity 1: has Position and Velocity
890        map.insert("entity_1", Position { x: 0.0, y: 0.0 });
891        map.insert("entity_1", Velocity { dx: 1.0, dy: 0.5 });
892
893        // Entity 2: has Position and Velocity
894        map.insert("entity_2", Position { x: 10.0, y: 20.0 });
895        map.insert("entity_2", Velocity { dx: -1.0, dy: -2.0 });
896
897        // Entity 3: only has Position
898        map.insert("entity_3", Position { x: 5.0, y: 5.0 });
899
900        // Update entities that have both components
901        for entity_id in &["entity_1", "entity_2", "entity_3"] {
902            map.with2_mut::<&str, Position, Velocity, _, _>(entity_id, |pos, vel| {
903                pos.x += vel.dx;
904                pos.y += vel.dy;
905            });
906        }
907
908        // Check results
909        let pos1 = map.get_cloned::<&str, Position>(&"entity_1").unwrap();
910        assert_eq!(pos1, Position { x: 1.0, y: 0.5 });
911
912        let pos2 = map.get_cloned::<&str, Position>(&"entity_2").unwrap();
913        assert_eq!(pos2, Position { x: 9.0, y: 18.0 });
914
915        // entity_3 didn't have Velocity so it wasn't updated
916        let pos3 = map.get_cloned::<&str, Position>(&"entity_3").unwrap();
917        assert_eq!(pos3, Position { x: 5.0, y: 5.0 });
918    }
919
920    #[test]
921    fn composition_ecs_physics_simulation() {
922        let map = CodexMap::new();
923
924        // Create some entities
925        let entities = vec!["ball", "player", "enemy"];
926
927        // Initialize components
928        map.insert("ball", Position { x: 0.0, y: 10.0 });
929        map.insert("ball", Velocity { dx: 5.0, dy: -2.0 });
930
931        map.insert("player", Position { x: 0.0, y: 0.0 });
932        map.insert("player", Velocity { dx: 1.0, dy: 0.0 });
933        map.insert("player", Health { hp: 100 });
934
935        map.insert("enemy", Position { x: 20.0, y: 5.0 });
936        map.insert("enemy", Velocity { dx: -0.5, dy: 0.0 });
937        map.insert("enemy", Health { hp: 50 });
938
939        // Simulate one physics tick
940        for entity in &entities {
941            // Update position based on velocity
942            map.with2_mut::<&str, Position, Velocity, _, _>(entity, |pos, vel| {
943                pos.x += vel.dx;
944                pos.y += vel.dy;
945            });
946        }
947
948        // Check final positions
949        let ball_pos = map.get_cloned::<&str, Position>(&"ball").unwrap();
950        assert_eq!(ball_pos, Position { x: 5.0, y: 8.0 });
951
952        let player_pos = map.get_cloned::<&str, Position>(&"player").unwrap();
953        assert_eq!(player_pos, Position { x: 1.0, y: 0.0 });
954
955        let enemy_pos = map.get_cloned::<&str, Position>(&"enemy").unwrap();
956        assert_eq!(enemy_pos, Position { x: 19.5, y: 5.0 });
957    }
958}