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