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