dynp/
property_collection.rs

1use std::any::{Any, TypeId};
2use std::collections::HashMap;
3use std::fmt::Debug;
4use crate::property::Property;
5
6/// Dynamic collection of properties.
7#[derive(Debug)]
8pub struct PropertyCollection {
9    properties: HashMap<TypeId, Box<dyn Any>>,
10}
11
12impl PropertyCollection {
13    pub fn new() -> Self {
14        Self {
15            properties: HashMap::new(),
16        }
17    }
18
19    /// Retrieves a reference to a property of a specific type from the PropertyCollection.
20    ///
21    /// # Returns
22    /// - `Some(&Property<T>)` if the property of type `T` exists and its type matches `T`.
23    /// - `None` if the property is not found or the property assigned to the TypeId of `T` does not
24    ///  match `T`.
25    #[inline]
26    fn get_property<T: Any>(&self) -> Option<&Property<T>> {
27        // Get the TypeId of the specified type T
28        let type_id = TypeId::of::<T>();
29
30        // Attempt to find the property by its TypeId
31        if let Some(property) = self.properties.get(&type_id) {
32            // Attempt to downcast the property to the specified type T
33            property
34                .downcast_ref::<Property<T>>()
35        } else {
36            // Property not found
37            None
38        }
39    }
40
41    /// Retrieves a mutable reference to a property of a specific type from the PropertyCollection.
42    ///
43    /// # Returns
44    /// - `Some(&mut Property<T>)` if the property of type `T` exists and its type matches `T`.
45    /// - `None` if the property is not found or the property assigned to the TypeId of `T` does not
46    ///  match `T`.
47    #[inline]
48    fn get_property_mut<T: Any>(&mut self) -> Option<&mut Property<T>> {
49        let type_id = TypeId::of::<T>();
50        if let Some(property) = self.properties.get_mut(&type_id) {
51            property
52                .downcast_mut::<Property<T>>()
53        } else {
54            None
55        }
56    }
57
58    /// Retrieves the value of a property of a specific type from the PropertyCollection.
59    ///
60    /// # Type Parameters
61    /// - `T`: The type of the property to retrieve. It should be a type that implements the `Any`
62    /// 
63    /// # Returns
64    /// - `Some(&T)` if the property of type `T` exists.
65    /// - `None` if the property is not found.
66    pub fn get<T: Any>(&self) -> Option<&T> {
67        if let Some(property) = self.get_property::<T>() {
68            property.get()
69        } else {
70            None
71        }
72    }
73
74    /// Assigns the value to a property or adds a new property with the specified value.
75    ///
76    /// This function assigns the provided `value` to an existing property of the specified type `T`
77    /// if it already exists in the collection. If no property of that type exists, a new property of
78    /// that type is added to the collection with the specified `value`.
79    ///
80    /// # Type Parameters
81    /// - `T`: The type of the property to assign. It should be a type that implements the `Any`
82    /// 
83    /// # Parameters
84    /// - `value`: The value of type `T` to assign to the property.
85    pub fn assign<T: Any>(&mut self, value: T)
86    {
87        if let Some(property) = self.get_property_mut::<T>() {
88            // if the property was found, just assign the new value
89            property.assign(value);
90        } else {
91            // if the property was not found, add it to the collection
92            self.properties.insert(
93                TypeId::of::<T>(),
94                Box::new(Property::new(value))
95            );
96        }
97    }
98
99    /// Checks if a property of a specified type exists in the collection.
100    ///
101    /// # Returns
102    /// * `true` - If a property of the specified type `T` exists in the collection.
103    /// * `false` - If no property of the specified type `T` is found in the collection.
104    pub fn contains<T: Any>(&self) -> bool {
105        self.get_property::<T>().is_some()
106    }
107
108    /// Subscribes to changes in a property of a specific type in the PropertyCollection.
109    ///
110    /// This function attempts to subscribe to changes in a property of the specified type `T` in the
111    /// `PropertyCollection`. If the property exists and its type matches `T`, it registers the provided
112    /// callback function to be called when the property's value changes. If the property does not
113    /// exist in the collection yet, an *early subscription* is performed by adding the callback to
114    /// ab empty property of type `T`. This allows the callback to be called when the property is
115    /// assigned a value later.
116    ///
117    ///
118    /// # Arguments
119    /// * `callback` - The callback function to be called when the property's value changes.
120    ///
121    /// # Example
122    ///
123    /// ```
124    /// use dynp::PropertyCollection;
125    ///
126    /// // define a custom property using the Newtype pattern
127    /// #[derive(Copy, Clone, Debug)]
128    /// struct CustomProperty(i32);
129    ///
130    /// fn main() {
131    ///     // create a new property collection
132    ///     let mut collection = PropertyCollection::new();
133    ///     collection.subscribe::<CustomProperty>(|value: &CustomProperty| {
134    ///         println!("Property changed: {:?}", value);
135    ///     });
136    ///
137    ///     // assign a new property
138    ///     collection.assign(CustomProperty(42));
139    /// }
140    /// ```
141    pub fn subscribe<T: Any>(
142        &mut self,
143        callback: impl FnMut(&T) + 'static
144    ) {
145        if let Some(property) = self.get_property_mut::<T>() {
146            // if the property exists, add the callback to its subscriptions
147            property.subscribe(Box::new(callback));
148        } else {
149            // if the property does not exist, add an empty property and add the callback to its
150            // subscriptions
151            let mut property: Property<T> = Property::empty();
152            property.subscribe(Box::new(callback));
153            self.properties.insert(
154                TypeId::of::<T>(),
155                Box::new(property)
156            );
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    /// A newtype wrapper for `i32` that implements the `ToOwned` trait.
166    #[derive(Copy, Clone, PartialEq, Eq, Debug)]
167    struct MyInt(i32);
168
169    #[test]
170    fn test_new_property_collection() {
171        let collection = PropertyCollection::new();
172        assert!(collection.properties.is_empty());
173    }
174
175    #[test]
176    fn test_assign_get_property_native() {
177        let mut collection = PropertyCollection::new();
178        collection.assign(42);
179
180        assert_eq!(collection.get::<i32>(), Some(&42));
181        assert_eq!(collection.get::<u32>(), None);
182        assert_eq!(collection.get::<String>(), None);
183
184
185        collection.assign(11);
186        assert_eq!(collection.get::<i32>(), Some(&11));
187        assert_eq!(collection.get::<u32>(), None);
188        assert_eq!(collection.get::<String>(), None);
189
190        // make sure the reference does not interfere with the original property
191        collection.assign(&99);
192        assert_eq!(collection.get::<i32>(), Some(&11));
193        assert_eq!(collection.get::<&i32>(), Some(&&99));
194        assert_eq!(collection.get::<u32>(), None);
195        assert_eq!(collection.get::<String>(), None);
196    }
197
198    #[test]
199    fn test_assign_get_property_newtype() {
200        let mut collection = PropertyCollection::new();
201        
202        collection.assign(MyInt(42));
203        assert_eq!(collection.get::<MyInt>(), Some(&MyInt(42)));
204        assert_eq!(collection.get::<i32>(), None);
205        assert_eq!(collection.get::<u32>(), None);
206    }
207
208    #[test]
209    fn test_contains_property_native() {
210        let mut collection = PropertyCollection::new();
211        assert!(!collection.contains::<i32>());
212
213        collection.assign(42);
214        collection.assign(&42);
215        assert!(collection.contains::<i32>());
216        assert!(!collection.contains::<String>());
217    }
218
219    #[test]
220    fn test_contains_property_newtype() {
221        let mut collection = PropertyCollection::new();
222        assert!(!collection.contains::<MyInt>());
223
224        collection.assign(MyInt(42));
225        assert!(collection.contains::<MyInt>());
226        assert!(!collection.contains::<i32>());
227        assert!(!collection.contains::<String>());
228    }
229
230    #[test]
231    fn test_early_subscribe() {
232        let mut collection = PropertyCollection::new();
233
234        let count = std::sync::Arc::new(std::sync::Mutex::new(0));
235        let count_clone = std::sync::Arc::clone(&count);
236
237        // create an early subscription
238        collection.subscribe::<i32>(move |value: &i32| {
239            assert_eq!(*value, 11);
240            let mut count = count_clone.lock().unwrap();
241            *count += 1;
242        });
243
244        // perform an assignment
245        collection.assign::<i32>(11);
246        assert_eq!(*count.lock().unwrap(), 1);
247
248        // perform a new assignment
249        collection.assign::<i32>(11);
250        assert_eq!(*count.lock().unwrap(), 2);
251    }
252
253    #[test]
254    fn test_subscribe() {
255        let mut collection = PropertyCollection::new();
256
257        let count = std::sync::Arc::new(std::sync::Mutex::new(0));
258        let count_clone = std::sync::Arc::clone(&count);
259
260        // perform an assignment
261        collection.assign::<i32>(11);
262        assert_eq!(*count.lock().unwrap(), 0);
263
264        // create an early subscription
265        collection.subscribe::<i32>(move |value: &i32| {
266            assert_eq!(*value, 11);
267            let mut count = count_clone.lock().unwrap();
268            *count += 1;
269        });
270
271        assert_eq!(*count.lock().unwrap(), 0);
272
273        // perform an assignment
274        collection.assign::<i32>(11);
275        assert_eq!(*count.lock().unwrap(), 1);
276
277        // perform an assignment
278        collection.assign::<i32>(11);
279        assert_eq!(*count.lock().unwrap(), 2);
280    }
281
282}