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}