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#[allow(clippy::type_complexity)]
38#[doc(hidden)]
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
52pub fn get_global_property_count() -> usize {
54 *NEXT_GLOBAL_PROPERTY_ID.lock().unwrap()
55}
56
57pub 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
123pub trait GlobalProperty: Any {
132 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 fn validate(value: &Self::Value) -> Result<(), Box<dyn Error + Send + Sync + 'static>>;
148}
149
150pub trait ContextGlobalPropertiesExt: ContextBase {
151 fn set_global_property_value<T: GlobalProperty + 'static>(
156 &mut self,
157 property: T,
158 value: T::Value,
159 ) -> Result<(), IxaError>;
160
161 fn get_global_property_value<T: GlobalProperty + 'static>(
163 &self,
164 _property: T,
165 ) -> Option<&T::Value>;
166
167 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 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 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 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 assert!(context
338 .set_global_property_value(DiseaseParams, params2.clone())
339 .is_err());
340
341 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, ¶ms);
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}