Skip to main content

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