1use 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#[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
48static NEXT_GLOBAL_PROPERTY_ID: Mutex<usize> = Mutex::new(0);
51
52#[must_use]
54pub fn get_global_property_count() -> usize {
55 *NEXT_GLOBAL_PROPERTY_ID.lock().unwrap()
56}
57
58#[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 trait GlobalProperty: Any {
133 type Value: Any;
135
136 #[must_use]
137 fn id() -> usize;
138
139 fn new() -> Self;
140
141 #[must_use]
142 fn name() -> &'static str {
143 let full = std::any::type_name::<Self>();
144 full.rsplit("::").next().unwrap()
145 }
146
147 fn validate(value: &Self::Value) -> Result<(), Box<dyn Error + Send + Sync + 'static>>;
151}
152
153pub trait ContextGlobalPropertiesExt: ContextBase {
154 fn set_global_property_value<T: GlobalProperty + 'static>(
159 &mut self,
160 property: T,
161 value: T::Value,
162 ) -> Result<(), IxaError>;
163
164 #[must_use]
166 fn get_global_property_value<T: GlobalProperty + 'static>(
167 &self,
168 _property: T,
169 ) -> Option<&T::Value>;
170
171 fn load_parameters_from_json<T: 'static + Debug + DeserializeOwned>(
179 &mut self,
180 file_name: &Path,
181 ) -> Result<T, IxaError> {
182 trace!("Loading parameters from JSON: {file_name:?}");
183 let config_file = fs::File::open(file_name)?;
184 let reader = BufReader::new(config_file);
185 let config = serde_json::from_reader(reader)?;
186 Ok(config)
187 }
188
189 fn load_global_properties(&mut self, file_name: &Path) -> Result<(), IxaError>;
213}
214impl ContextGlobalPropertiesExt for Context {
215 fn set_global_property_value<T: GlobalProperty + 'static>(
216 &mut self,
217 _property: T,
218 value: T::Value,
219 ) -> Result<(), IxaError> {
220 T::validate(&value).map_err(|source| IxaError::IllegalGlobalPropertyValue {
221 name: T::name().to_string(),
222 source,
223 })?;
224 let index = T::id();
225 let cell = self.global_properties.get_mut(index).unwrap_or_else(|| {
226 panic!(
227 "No global property found with index = {index:?}. You must use the \
228 `define_global_property!` macro to create a global property."
229 )
230 });
231 if cell.get().is_some() {
232 return Err(IxaError::EntryAlreadyExists);
236 }
237 let _ = cell.set(Box::new(value));
238 Ok(())
239 }
240
241 fn get_global_property_value<T: GlobalProperty + 'static>(
242 &self,
243 _property: T,
244 ) -> Option<&T::Value> {
245 let index = T::id();
246 self.global_properties
247 .get(index)
248 .unwrap_or_else(|| {
249 panic!(
250 "No global property found with index = {index:?}. You must use the \
251 `define_global_property!` macro to create a global property."
252 )
253 })
254 .get()
255 .map(|property| {
256 property.downcast_ref::<T::Value>().expect(
257 "TypeID does not match global property type. You must use the \
258 `define_global_property!` macro to create a global property.",
259 )
260 })
261 }
262
263 fn load_global_properties(&mut self, file_name: &Path) -> Result<(), IxaError> {
264 trace!("Loading global properties from {file_name:?}");
265 let config_file = fs::File::open(file_name)?;
266 let reader = BufReader::new(config_file);
267 let val: serde_json::Map<String, serde_json::Value> = serde_json::from_reader(reader)?;
268
269 for (k, v) in val {
270 if let Some(setter) = get_global_property_setter_for_config_key(&k) {
271 setter(self, &k, v)?;
272 } else {
273 return Err(IxaError::NoGlobalProperty { name: k });
274 }
275 }
276
277 Ok(())
278 }
279}
280
281#[cfg(test)]
282mod test {
283 use std::error::Error;
284 use std::fmt;
285 use std::path::PathBuf;
286
287 use serde::{Deserialize, Serialize};
288 use tempfile::tempdir;
289
290 use super::*;
291 use crate::context::Context;
292 use crate::define_global_property;
293 use crate::error::IxaError;
294
295 fn fixture_path(name: &str) -> PathBuf {
296 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
297 .join("integration-tests/fixtures/global-properties")
298 .join(name)
299 }
300
301 #[derive(Debug)]
302 struct InvalidProperty3Value {
303 field_int: u32,
304 }
305
306 impl fmt::Display for InvalidProperty3Value {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 write!(f, "field_int must be zero, got {}", self.field_int)
309 }
310 }
311
312 impl Error for InvalidProperty3Value {}
313
314 #[derive(Serialize, Deserialize, Debug, Clone)]
315 pub struct ParamType {
316 pub days: usize,
317 pub diseases: usize,
318 }
319
320 define_global_property!(DiseaseParams, ParamType);
321
322 #[test]
323 fn set_get_global_property() {
324 let params: ParamType = ParamType {
325 days: 10,
326 diseases: 2,
327 };
328 let params2: ParamType = ParamType {
329 days: 11,
330 diseases: 3,
331 };
332
333 let mut context = Context::new();
334
335 context
337 .set_global_property_value(DiseaseParams, params.clone())
338 .unwrap();
339 let global_params = context
340 .get_global_property_value(DiseaseParams)
341 .unwrap()
342 .clone();
343 assert_eq!(global_params.days, params.days);
344 assert_eq!(global_params.diseases, params.diseases);
345
346 assert!(context
348 .set_global_property_value(DiseaseParams, params2.clone())
349 .is_err());
350
351 let global_params = context
353 .get_global_property_value(DiseaseParams)
354 .unwrap()
355 .clone();
356 assert_eq!(global_params.days, params.days);
357 assert_eq!(global_params.diseases, params.diseases);
358 }
359
360 #[test]
361 fn get_global_propert_missing() {
362 let context = Context::new();
363 let global_params = context.get_global_property_value(DiseaseParams);
364 assert!(global_params.is_none());
365 }
366
367 #[test]
368 fn set_parameters() {
369 let mut context = Context::new();
370 let temp_dir = tempdir().unwrap();
371 let config_path = PathBuf::from(&temp_dir.path());
372 let file_name = "test.json";
373 let file_path = config_path.join(file_name);
374 let config = fs::File::create(config_path.join(file_name)).unwrap();
375
376 let params: ParamType = ParamType {
377 days: 10,
378 diseases: 2,
379 };
380
381 define_global_property!(Parameters, ParamType);
382
383 let _ = serde_json::to_writer(config, ¶ms);
384 let params_json = context
385 .load_parameters_from_json::<ParamType>(&file_path)
386 .unwrap();
387
388 context
389 .set_global_property_value(Parameters, params_json)
390 .unwrap();
391
392 let params_read = context
393 .get_global_property_value(Parameters)
394 .unwrap()
395 .clone();
396 assert_eq!(params_read.days, params.days);
397 assert_eq!(params_read.diseases, params.diseases);
398 }
399
400 #[derive(Serialize, Deserialize)]
401 pub struct Property1Type {
402 field_int: u32,
403 field_str: String,
404 }
405 define_global_property!(Property1, Property1Type);
406
407 #[derive(Serialize, Deserialize)]
408 pub struct Property2Type {
409 field_int: u32,
410 }
411 define_global_property!(Property2, Property2Type);
412
413 #[test]
414 fn read_global_properties() {
415 let mut context = Context::new();
416 let path = fixture_path("global_properties_test1.json");
417 context.load_global_properties(&path).unwrap();
418 let p1 = context.get_global_property_value(Property1).unwrap();
419 assert_eq!(p1.field_int, 1);
420 assert_eq!(p1.field_str, "test");
421 let p2 = context.get_global_property_value(Property2).unwrap();
422 assert_eq!(p2.field_int, 2);
423 }
424
425 #[test]
426 fn read_unknown_property() {
427 let mut context = Context::new();
428 let path = fixture_path("global_properties_missing.json");
429 match context.load_global_properties(&path) {
430 Err(IxaError::NoGlobalProperty { name }) => assert_eq!(name, "ixa.PropertyUnknown"),
431 _ => panic!("Unexpected error type"),
432 }
433 }
434
435 #[test]
436 fn read_malformed_property() {
437 let mut context = Context::new();
438 let path = fixture_path("global_properties_malformed.json");
439 let error = context.load_global_properties(&path);
440 match error {
441 Err(IxaError::JsonError(_)) => {}
442 _ => panic!("Unexpected error type"),
443 }
444 }
445
446 #[test]
447 fn read_duplicate_property() {
448 let mut context = Context::new();
449 let path = fixture_path("global_properties_test1.json");
450 context.load_global_properties(&path).unwrap();
451 let error = context.load_global_properties(&path);
452 match error {
453 Err(IxaError::DuplicateProperty { .. }) => {}
454 _ => panic!("Unexpected error type"),
455 }
456 }
457
458 #[derive(Serialize, Deserialize)]
459 pub struct Property3Type {
460 field_int: u32,
461 }
462 define_global_property!(Property3, Property3Type, |v: &Property3Type| {
463 match v.field_int {
464 0 => Ok(()),
465 _ => Err(Box::new(InvalidProperty3Value {
466 field_int: v.field_int,
467 }) as Box<dyn Error + Send + Sync + 'static>),
468 }
469 });
470
471 #[test]
472 fn validate_property_set_success() {
473 let mut context = Context::new();
474 context
475 .set_global_property_value(Property3, Property3Type { field_int: 0 })
476 .unwrap();
477 }
478
479 #[test]
480 fn validate_property_set_failure() {
481 let mut context = Context::new();
482 let error = context
483 .set_global_property_value(Property3, Property3Type { field_int: 1 })
484 .unwrap_err();
485 assert_eq!(
486 error.to_string(),
487 "illegal value for global property `Property3`: field_int must be zero, got 1"
488 );
489 match error {
490 IxaError::IllegalGlobalPropertyValue { name, source } => {
491 assert_eq!(name, "Property3");
492 assert_eq!(source.to_string(), "field_int must be zero, got 1");
493 }
494 _ => panic!("Unexpected error type"),
495 }
496 }
497
498 #[test]
499 fn validate_property_load_success() {
500 let mut context = Context::new();
501 let path = fixture_path("global_properties_valid.json");
502 context.load_global_properties(&path).unwrap();
503 }
504
505 #[test]
506 fn validate_property_load_failure() {
507 let mut context = Context::new();
508 let path = fixture_path("global_properties_invalid.json");
509 let error = context.load_global_properties(&path).unwrap_err();
510 assert_eq!(
511 error.to_string(),
512 "illegal value for global property `Property3`: field_int must be zero, got 42"
513 );
514 match error {
515 IxaError::IllegalGlobalPropertyValue { name, source } => {
516 assert_eq!(name, "Property3");
517 assert_eq!(source.to_string(), "field_int must be zero, got 42");
518 }
519 _ => panic!("Unexpected error type"),
520 }
521 }
522}