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(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
139pub trait GlobalProperty: Any {
148 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 full.rsplit("::").next().unwrap()
161 }
162
163 fn validate(value: &Self::Value) -> Result<(), Box<dyn Error + Send + Sync + 'static>>;
167}
168
169pub trait ContextGlobalPropertiesExt: ContextBase {
170 fn set_global_property_value<T: GlobalProperty + 'static>(
180 &mut self,
181 property: T,
182 value: T::Value,
183 ) -> Result<(), IxaError>;
184
185 #[must_use]
192 fn get_global_property_value<T: GlobalProperty + 'static>(
193 &self,
194 _property: T,
195 ) -> Option<&T::Value>;
196
197 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 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 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 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 assert!(context
371 .set_global_property_value(DiseaseParams, params2.clone())
372 .is_err());
373
374 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, ¶ms);
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}