ixa/
global_properties.rs

1//! A generic mechanism for storing context-wide data.
2//!
3//! Global properties are not mutable and represent variables that are
4//! required in a global scope during the simulation, such as
5//! simulation parameters.
6//! A global property can be of any type, and is is just a value
7//! stored in the context. Global properties are defined by the
8//! [`crate::define_global_property!()`] macro and can then be
9//! set in one of two ways:
10//!
11//! * Directly by using [`Context::set_global_property_value()`]
12//! * Loaded from a configuration file using [`Context::load_global_properties()`]
13//!
14//! Attempting to change a global property which has been set already
15//! will result in an error.
16//!
17//! Global properties can be read with [`Context::get_global_property_value()`]
18use std::any::{Any, TypeId};
19use std::cell::RefCell;
20use std::collections::hash_map::Entry;
21use std::fmt::Debug;
22use std::fs;
23use std::io::BufReader;
24use std::path::Path;
25use std::sync::{Arc, LazyLock, Mutex};
26
27use serde::de::DeserializeOwned;
28
29use crate::context::Context;
30use crate::error::IxaError;
31use crate::{define_data_plugin, trace, ContextBase, HashMap, HashMapExt};
32
33type PropertySetterFn =
34    dyn Fn(&mut Context, &str, serde_json::Value) -> Result<(), IxaError> + Send + Sync;
35
36type PropertyGetterFn = dyn Fn(&Context) -> Result<Option<String>, IxaError> + Send + Sync;
37
38pub struct PropertyAccessors {
39    setter: Box<PropertySetterFn>,
40    getter: Box<PropertyGetterFn>,
41}
42
43#[allow(clippy::type_complexity)]
44// This is a global list of all the global properties that
45// are compiled in. Fundamentally it's a HashMap of property
46// names to the setter function, but it's wrapped in the
47// RefCell/Mutex/LazyLock combo to allow it to be globally
48// shared and initialized at startup time while still being
49// safe.
50#[doc(hidden)]
51pub static GLOBAL_PROPERTIES: LazyLock<Mutex<RefCell<HashMap<String, Arc<PropertyAccessors>>>>> =
52    LazyLock::new(|| Mutex::new(RefCell::new(HashMap::new())));
53
54#[allow(clippy::missing_panics_doc)]
55pub fn add_global_property<T: GlobalProperty>(name: &str)
56where
57    for<'de> <T as GlobalProperty>::Value: serde::Deserialize<'de> + serde::Serialize,
58{
59    trace!("Adding global property {name}");
60    let properties = GLOBAL_PROPERTIES.lock().unwrap();
61    properties
62        .borrow_mut()
63        .insert(
64            name.to_string(),
65            Arc::new(PropertyAccessors {
66                setter: Box::new(
67                    |context: &mut Context, name, value| -> Result<(), IxaError> {
68                        let val: T::Value = serde_json::from_value(value)?;
69                        T::validate(&val)?;
70                        if context.get_global_property_value(T::new()).is_some() {
71                            return Err(IxaError::IxaError(format!("Duplicate property {name}")));
72                        }
73                        context.set_global_property_value(T::new(), val)?;
74                        Ok(())
75                    },
76                ),
77                getter: Box::new(|context: &Context| -> Result<Option<String>, IxaError> {
78                    let value = context.get_global_property_value(T::new());
79                    match value {
80                        Some(val) => Ok(Some(serde_json::to_string(val)?)),
81                        None => Ok(None),
82                    }
83                }),
84            }),
85        )
86        .inspect(|_| panic!("Duplicate global property {}", name));
87}
88
89fn get_global_property_accessor(name: &str) -> Option<Arc<PropertyAccessors>> {
90    let properties = GLOBAL_PROPERTIES.lock().unwrap();
91    let tmp = properties.borrow();
92    tmp.get(name).map(Arc::clone)
93}
94
95/// The trait representing a global property. Do not use this
96/// directly, but instead define global properties with
97/// [`define_global_property!`](crate::define_global_property!)
98pub trait GlobalProperty: Any {
99    type Value: Any; // The actual type of the data.
100
101    fn new() -> Self;
102    #[allow(clippy::missing_errors_doc)]
103    // A function which validates the global property.
104    fn validate(value: &Self::Value) -> Result<(), IxaError>;
105}
106
107struct GlobalPropertiesDataContainer {
108    global_property_container: HashMap<TypeId, Box<dyn Any>>,
109}
110
111define_data_plugin!(
112    GlobalPropertiesPlugin,
113    GlobalPropertiesDataContainer,
114    GlobalPropertiesDataContainer {
115        global_property_container: HashMap::default(),
116    }
117);
118
119impl GlobalPropertiesDataContainer {
120    fn set_global_property_value<T: GlobalProperty + 'static>(
121        &mut self,
122        _property: &T,
123        value: T::Value,
124    ) -> Result<(), IxaError> {
125        match self.global_property_container.entry(TypeId::of::<T>()) {
126            Entry::Vacant(entry) => {
127                entry.insert(Box::new(value));
128                Ok(())
129            }
130            // Note: If we change global properties to be mutable, we'll need to
131            // update define_derived_property to either handle updates or only
132            // allow immutable properties.
133            Entry::Occupied(_) => Err(IxaError::from("Entry already exists")),
134        }
135    }
136
137    #[must_use]
138    fn get_global_property_value<T: GlobalProperty + 'static>(&self) -> Option<&T::Value> {
139        let data_container = self.global_property_container.get(&TypeId::of::<T>());
140
141        match data_container {
142            Some(property) => Some(property.downcast_ref::<T::Value>().unwrap()),
143            None => None,
144        }
145    }
146}
147
148pub trait ContextGlobalPropertiesExt: ContextBase {
149    /// Set the value of a global property of type T
150    ///
151    /// # Errors
152    /// Will return an error if an attempt is made to change a value.
153    fn set_global_property_value<T: GlobalProperty + 'static>(
154        &mut self,
155        property: T,
156        value: T::Value,
157    ) -> Result<(), IxaError> {
158        T::validate(&value)?;
159        let data_container = self.get_data_mut(GlobalPropertiesPlugin);
160        data_container.set_global_property_value(&property, value)
161    }
162
163    /// Return value of global property T
164    #[allow(unused_variables)]
165    fn get_global_property_value<T: GlobalProperty + 'static>(
166        &self,
167        _property: T,
168    ) -> Option<&T::Value> {
169        self.get_data(GlobalPropertiesPlugin)
170            .get_global_property_value::<T>()
171    }
172
173    fn list_registered_global_properties(&self) -> Vec<String> {
174        let properties = GLOBAL_PROPERTIES.lock().unwrap();
175        let tmp = properties.borrow();
176        tmp.keys().cloned().collect()
177    }
178
179    /// Return the serialized value of a global property by fully qualified name
180    ///
181    /// # Errors
182    ///
183    /// Will return an [`IxaError`] if the property does not exist
184    fn get_serialized_value_by_string(&self, name: &str) -> Result<Option<String>, IxaError>;
185
186    /// Given a file path for a valid json file, deserialize parameter values
187    /// for a given struct T
188    ///
189    /// # Errors
190    ///
191    /// Will return an [`IxaError`] if the `file_path` does not exist or
192    /// cannot be deserialized
193    fn load_parameters_from_json<T: 'static + Debug + DeserializeOwned>(
194        &mut self,
195        file_name: &Path,
196    ) -> Result<T, IxaError> {
197        trace!("Loading parameters from JSON: {file_name:?}");
198        let config_file = fs::File::open(file_name)?;
199        let reader = BufReader::new(config_file);
200        let config = serde_json::from_reader(reader)?;
201        Ok(config)
202    }
203
204    /// Load global properties from a JSON file.
205    ///
206    /// The expected structure is a dictionary with each name being
207    /// the name of the struct prefixed with the crate name, as in:
208    /// `ixa.NumFluVariants` and the value being an object which can
209    /// serde deserialize into the relevant struct.
210    ///
211    /// # Errors
212    /// Will return an [`IxaError`] if:
213    /// * The `file_path` doesn't exist
214    /// * The file isn't valid JSON
215    /// * A specified object doesn't correspond to an existing global property.
216    /// * There are two values for the same object.
217    ///
218    /// Ixa automatically knows about any property defined with
219    /// [`define_global_property!`](crate::define_global_property) so you don't need to register them
220    /// explicitly.
221    ///
222    /// It is possible to call [`Context::load_global_properties()`] multiple
223    /// times with different files as long as the files have disjoint
224    /// sets of properties.
225    fn load_global_properties(&mut self, file_name: &Path) -> Result<(), IxaError>;
226}
227impl ContextGlobalPropertiesExt for Context {
228    fn get_serialized_value_by_string(&self, name: &str) -> Result<Option<String>, IxaError> {
229        let accessor = get_global_property_accessor(name);
230        match accessor {
231            Some(accessor) => (accessor.getter)(self),
232            None => Err(IxaError::from(format!("No global property: {name}"))),
233        }
234    }
235
236    fn load_global_properties(&mut self, file_name: &Path) -> Result<(), IxaError> {
237        trace!("Loading global properties from {file_name:?}");
238        let config_file = fs::File::open(file_name)?;
239        let reader = BufReader::new(config_file);
240        let val: serde_json::Map<String, serde_json::Value> = serde_json::from_reader(reader)?;
241
242        for (k, v) in val {
243            if let Some(accessor) = get_global_property_accessor(&k) {
244                (accessor.setter)(self, &k, v)?;
245            } else {
246                return Err(IxaError::from(format!("No global property: {k}")));
247            }
248        }
249
250        Ok(())
251    }
252}
253
254#[cfg(test)]
255mod test {
256    use std::path::PathBuf;
257
258    use serde::{Deserialize, Serialize};
259    use tempfile::tempdir;
260
261    use super::*;
262    use crate::context::Context;
263    use crate::define_global_property;
264    use crate::error::IxaError;
265
266    #[derive(Serialize, Deserialize, Debug, Clone)]
267    pub struct ParamType {
268        pub days: usize,
269        pub diseases: usize,
270    }
271
272    define_global_property!(DiseaseParams, ParamType);
273
274    #[test]
275    fn set_get_global_property() {
276        let params: ParamType = ParamType {
277            days: 10,
278            diseases: 2,
279        };
280        let params2: ParamType = ParamType {
281            days: 11,
282            diseases: 3,
283        };
284
285        let mut context = Context::new();
286
287        // Set and check the stored value.
288        context
289            .set_global_property_value(DiseaseParams, params.clone())
290            .unwrap();
291        let global_params = context
292            .get_global_property_value(DiseaseParams)
293            .unwrap()
294            .clone();
295        assert_eq!(global_params.days, params.days);
296        assert_eq!(global_params.diseases, params.diseases);
297
298        // Setting again should fail because global properties are immutable.
299        assert!(context
300            .set_global_property_value(DiseaseParams, params2.clone())
301            .is_err());
302
303        // Check that the value is unchanged.
304        let global_params = context
305            .get_global_property_value(DiseaseParams)
306            .unwrap()
307            .clone();
308        assert_eq!(global_params.days, params.days);
309        assert_eq!(global_params.diseases, params.diseases);
310    }
311
312    #[test]
313    fn get_global_propert_missing() {
314        let context = Context::new();
315        let global_params = context.get_global_property_value(DiseaseParams);
316        assert!(global_params.is_none());
317    }
318
319    #[test]
320    fn set_parameters() {
321        let mut context = Context::new();
322        let temp_dir = tempdir().unwrap();
323        let config_path = PathBuf::from(&temp_dir.path());
324        let file_name = "test.json";
325        let file_path = config_path.join(file_name);
326        let config = fs::File::create(config_path.join(file_name)).unwrap();
327
328        let params: ParamType = ParamType {
329            days: 10,
330            diseases: 2,
331        };
332
333        define_global_property!(Parameters, ParamType);
334
335        let _ = serde_json::to_writer(config, &params);
336        let params_json = context
337            .load_parameters_from_json::<ParamType>(&file_path)
338            .unwrap();
339
340        context
341            .set_global_property_value(Parameters, params_json)
342            .unwrap();
343
344        let params_read = context
345            .get_global_property_value(Parameters)
346            .unwrap()
347            .clone();
348        assert_eq!(params_read.days, params.days);
349        assert_eq!(params_read.diseases, params.diseases);
350    }
351
352    #[derive(Serialize, Deserialize)]
353    pub struct Property1Type {
354        field_int: u32,
355        field_str: String,
356    }
357    define_global_property!(Property1, Property1Type);
358
359    #[derive(Serialize, Deserialize)]
360    pub struct Property2Type {
361        field_int: u32,
362    }
363    define_global_property!(Property2, Property2Type);
364
365    #[test]
366    fn read_global_properties() {
367        let mut context = Context::new();
368        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
369            .join("tests/data/global_properties_test1.json");
370        context.load_global_properties(&path).unwrap();
371        let p1 = context.get_global_property_value(Property1).unwrap();
372        assert_eq!(p1.field_int, 1);
373        assert_eq!(p1.field_str, "test");
374        let p2 = context.get_global_property_value(Property2).unwrap();
375        assert_eq!(p2.field_int, 2);
376    }
377
378    #[test]
379    fn read_unknown_property() {
380        let mut context = Context::new();
381        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
382            .join("tests/data/global_properties_missing.json");
383        match context.load_global_properties(&path) {
384            Err(IxaError::IxaError(msg)) => {
385                assert_eq!(msg, "No global property: ixa.PropertyUnknown");
386            }
387            _ => panic!("Unexpected error type"),
388        }
389    }
390
391    #[test]
392    fn read_malformed_property() {
393        let mut context = Context::new();
394        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
395            .join("tests/data/global_properties_malformed.json");
396        let error = context.load_global_properties(&path);
397        println!("Error {error:?}");
398        match error {
399            Err(IxaError::JsonError(_)) => {}
400            _ => panic!("Unexpected error type"),
401        }
402    }
403
404    #[test]
405    fn read_duplicate_property() {
406        let mut context = Context::new();
407        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
408            .join("tests/data/global_properties_test1.json");
409        context.load_global_properties(&path).unwrap();
410        let error = context.load_global_properties(&path);
411        match error {
412            Err(IxaError::IxaError(_)) => {}
413            _ => panic!("Unexpected error type"),
414        }
415    }
416
417    #[derive(Serialize, Deserialize)]
418    pub struct Property3Type {
419        field_int: u32,
420    }
421    define_global_property!(Property3, Property3Type, |v: &Property3Type| {
422        match v.field_int {
423            0 => Ok(()),
424            _ => Err(IxaError::IxaError(format!(
425                "Illegal value for `field_int`: {}",
426                v.field_int
427            ))),
428        }
429    });
430
431    #[test]
432    fn validate_property_set_success() {
433        let mut context = Context::new();
434        context
435            .set_global_property_value(Property3, Property3Type { field_int: 0 })
436            .unwrap();
437    }
438
439    #[test]
440    fn validate_property_set_failure() {
441        let mut context = Context::new();
442        assert!(matches!(
443            context.set_global_property_value(Property3, Property3Type { field_int: 1 }),
444            Err(IxaError::IxaError(_))
445        ));
446    }
447
448    #[test]
449    fn validate_property_load_success() {
450        let mut context = Context::new();
451        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
452            .join("tests/data/global_properties_valid.json");
453        context.load_global_properties(&path).unwrap();
454    }
455
456    #[test]
457    fn validate_property_load_failure() {
458        let mut context = Context::new();
459        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
460            .join("tests/data/global_properties_invalid.json");
461        assert!(matches!(
462            context.load_global_properties(&path),
463            Err(IxaError::IxaError(_))
464        ));
465    }
466
467    #[test]
468    fn list_registered_global_properties() {
469        let context = Context::new();
470        let properties = context.list_registered_global_properties();
471        assert!(properties.contains(&"ixa.DiseaseParams".to_string()));
472    }
473
474    #[test]
475    fn get_serialized_value_by_string() {
476        let mut context = Context::new();
477        context
478            .set_global_property_value(
479                DiseaseParams,
480                ParamType {
481                    days: 10,
482                    diseases: 2,
483                },
484            )
485            .unwrap();
486        let serialized = context
487            .get_serialized_value_by_string("ixa.DiseaseParams")
488            .unwrap();
489        assert_eq!(serialized, Some("{\"days\":10,\"diseases\":2}".to_string()));
490    }
491}