Skip to main content

nv_core/
metadata.rs

1//! Type-map metadata container.
2//!
3//! [`TypedMetadata`] stores arbitrary `Send + Sync + 'static` values keyed by
4//! their concrete [`TypeId`]. At most one value per concrete type. This is the
5//! standard "type-map" or "AnyMap" pattern.
6//!
7//! Used on `FrameEnvelope`, `Detection`, `Track`, `OutputEnvelope`,
8//! and `PerceptionArtifacts` to carry extensible domain-specific data
9//! without modifying core types.
10//!
11//! # Cloning cost
12//!
13//! `TypedMetadata::clone()` deep-clones every stored value. Metadata bags are
14//! typically small (2–5 entries) with lightweight types. If a stage stores large
15//! data (e.g., a full feature map), wrap it in `Arc<T>` so that cloning the bag
16//! clones only the `Arc`, not the data.
17
18use std::any::{Any, TypeId};
19use std::collections::HashMap;
20
21// ---------------------------------------------------------------------------
22// Internal cloneable entry — stores a type-erased value together with a
23// function pointer that knows how to deep-clone it.  This avoids a custom
24// `CloneableAny` trait whose `as_any` return trips borrow-checker lifetime
25// issues.
26// ---------------------------------------------------------------------------
27
28struct CloneableEntry {
29    value: Box<dyn Any + Send + Sync>,
30    clone_fn: fn(&(dyn Any + Send + Sync)) -> Box<dyn Any + Send + Sync>,
31}
32
33fn make_clone_fn<T: Clone + Send + Sync + 'static>()
34-> fn(&(dyn Any + Send + Sync)) -> Box<dyn Any + Send + Sync> {
35    |any| {
36        // Safety-net: downcast must succeed because the clone_fn is monomorphised
37        // for the same T that was inserted. A failure here indicates memory
38        // corruption or a logic bug — log and abort rather than panicking with
39        // an opaque message in production.
40        let val = match any.downcast_ref::<T>() {
41            Some(v) => v,
42            None => {
43                // This branch is structurally unreachable: the clone_fn is
44                // always paired with the correct type at insertion time.
45                // If it ever fires, something is deeply wrong.
46                unreachable!(
47                    "TypedMetadata clone: type mismatch for TypeId {:?}",
48                    std::any::TypeId::of::<T>()
49                );
50            }
51        };
52        Box::new(val.clone())
53    }
54}
55
56impl Clone for CloneableEntry {
57    fn clone(&self) -> Self {
58        Self {
59            value: (self.clone_fn)(&*self.value),
60            clone_fn: self.clone_fn,
61        }
62    }
63}
64
65/// Typed metadata bag — stores arbitrary `Send + Sync + 'static` values
66/// keyed by their concrete type.
67///
68/// At most one value per concrete type. To store multiple values of the same
69/// underlying type, use distinct newtype wrappers.
70///
71/// # Example
72///
73/// ```
74/// use nv_core::TypedMetadata;
75///
76/// #[derive(Clone, Debug, PartialEq)]
77/// struct DetectorScore(f32);
78///
79/// #[derive(Clone, Debug, PartialEq)]
80/// struct TrackerScore(f32);
81///
82/// let mut meta = TypedMetadata::new();
83/// meta.insert(DetectorScore(0.95));
84/// meta.insert(TrackerScore(0.8));
85///
86/// assert_eq!(meta.get::<DetectorScore>(), Some(&DetectorScore(0.95)));
87/// assert_eq!(meta.get::<TrackerScore>(), Some(&TrackerScore(0.8)));
88/// assert_eq!(meta.len(), 2);
89/// ```
90pub struct TypedMetadata {
91    map: HashMap<TypeId, CloneableEntry>,
92}
93
94impl TypedMetadata {
95    /// Create an empty metadata bag.
96    #[must_use]
97    pub fn new() -> Self {
98        Self {
99            map: HashMap::new(),
100        }
101    }
102
103    /// Insert a value. If a value of this type already exists, it is replaced
104    /// and the old value is returned.
105    pub fn insert<T: Clone + Send + Sync + 'static>(&mut self, val: T) -> Option<T> {
106        let entry = CloneableEntry {
107            value: Box::new(val),
108            clone_fn: make_clone_fn::<T>(),
109        };
110        self.map
111            .insert(TypeId::of::<T>(), entry)
112            .and_then(|old| old.value.downcast::<T>().ok())
113            .map(|b| *b)
114    }
115
116    /// Get a reference to the stored value of type `T`, if present.
117    #[must_use]
118    pub fn get<T: 'static>(&self) -> Option<&T> {
119        let entry = self.map.get(&TypeId::of::<T>())?;
120        entry.value.downcast_ref()
121    }
122
123    /// Get a mutable reference to the stored value of type `T`, if present.
124    pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
125        let entry = self.map.get_mut(&TypeId::of::<T>())?;
126        entry.value.downcast_mut()
127    }
128
129    /// Remove and return the stored value of type `T`, if present.
130    pub fn remove<T: 'static>(&mut self) -> Option<T> {
131        self.map
132            .remove(&TypeId::of::<T>())
133            .and_then(|old| old.value.downcast::<T>().ok())
134            .map(|b| *b)
135    }
136
137    /// Returns `true` if a value of type `T` is stored.
138    #[must_use]
139    pub fn contains<T: 'static>(&self) -> bool {
140        self.map.contains_key(&TypeId::of::<T>())
141    }
142
143    /// Number of entries.
144    #[must_use]
145    pub fn len(&self) -> usize {
146        self.map.len()
147    }
148
149    /// Returns `true` if the metadata bag is empty.
150    #[must_use]
151    pub fn is_empty(&self) -> bool {
152        self.map.is_empty()
153    }
154
155    /// Merge another `TypedMetadata` into this one.
156    ///
157    /// Keys present in `other` overwrite keys in `self` (last-writer-wins).
158    pub fn merge(&mut self, other: TypedMetadata) {
159        self.map.extend(other.map);
160    }
161}
162
163impl Clone for TypedMetadata {
164    fn clone(&self) -> Self {
165        Self {
166            map: self.map.clone(),
167        }
168    }
169}
170
171impl Default for TypedMetadata {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177impl fmt::Debug for TypedMetadata {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        f.debug_struct("TypedMetadata")
180            .field("len", &self.map.len())
181            .finish()
182    }
183}
184
185use std::fmt;
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[derive(Clone, Debug, PartialEq)]
192    struct Foo(u32);
193
194    #[derive(Clone, Debug, PartialEq)]
195    struct Bar(String);
196
197    #[test]
198    fn insert_and_get() {
199        let mut m = TypedMetadata::new();
200        m.insert(Foo(42));
201        assert_eq!(m.get::<Foo>(), Some(&Foo(42)));
202        assert_eq!(m.get::<Bar>(), None);
203    }
204
205    #[test]
206    fn replace_returns_old() {
207        let mut m = TypedMetadata::new();
208        assert!(m.insert(Foo(1)).is_none());
209        let old = m.insert(Foo(2));
210        assert_eq!(old, Some(Foo(1)));
211        assert_eq!(m.get::<Foo>(), Some(&Foo(2)));
212    }
213
214    #[test]
215    fn remove() {
216        let mut m = TypedMetadata::new();
217        m.insert(Foo(10));
218        let removed = m.remove::<Foo>();
219        assert_eq!(removed, Some(Foo(10)));
220        assert!(!m.contains::<Foo>());
221    }
222
223    #[test]
224    fn clone_is_deep() {
225        let mut m = TypedMetadata::new();
226        m.insert(Bar("hello".into()));
227        let m2 = m.clone();
228        m.insert(Bar("changed".into()));
229        assert_eq!(m2.get::<Bar>(), Some(&Bar("hello".into())));
230    }
231
232    #[test]
233    fn merge_overwrites() {
234        let mut a = TypedMetadata::new();
235        a.insert(Foo(1));
236        let mut b = TypedMetadata::new();
237        b.insert(Foo(2));
238        b.insert(Bar("from_b".into()));
239        a.merge(b);
240        assert_eq!(a.get::<Foo>(), Some(&Foo(2)));
241        assert_eq!(a.get::<Bar>(), Some(&Bar("from_b".into())));
242    }
243}