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