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