1use crate::context::Context;
19use crate::error::IxaError;
20use crate::trace;
21use crate::{HashMap, HashMapExt};
22use serde::de::DeserializeOwned;
23use std::any::{Any, TypeId};
24use std::cell::RefCell;
25use std::collections::hash_map::Entry;
26use std::fmt::Debug;
27use std::fs;
28use std::io::BufReader;
29use std::path::Path;
30use std::sync::Arc;
31use std::sync::LazyLock;
32use std::sync::Mutex;
33
34type PropertySetterFn =
35 dyn Fn(&mut Context, &str, serde_json::Value) -> Result<(), IxaError> + Send + Sync;
36
37type PropertyGetterFn = dyn Fn(&Context) -> Result<Option<String>, IxaError> + Send + Sync;
38
39pub struct PropertyAccessors {
40 setter: Box<PropertySetterFn>,
41 getter: Box<PropertyGetterFn>,
42}
43
44#[allow(clippy::type_complexity)]
45#[doc(hidden)]
52pub static GLOBAL_PROPERTIES: LazyLock<Mutex<RefCell<HashMap<String, Arc<PropertyAccessors>>>>> =
53 LazyLock::new(|| Mutex::new(RefCell::new(HashMap::new())));
54
55#[allow(clippy::missing_panics_doc)]
56pub fn add_global_property<T: GlobalProperty>(name: &str)
57where
58 for<'de> <T as GlobalProperty>::Value: serde::Deserialize<'de> + serde::Serialize,
59{
60 trace!("Adding global property {name}");
61 let properties = GLOBAL_PROPERTIES.lock().unwrap();
62 properties
63 .borrow_mut()
64 .insert(
65 name.to_string(),
66 Arc::new(PropertyAccessors {
67 setter: Box::new(
68 |context: &mut Context, name, value| -> Result<(), IxaError> {
69 let val: T::Value = serde_json::from_value(value)?;
70 T::validate(&val)?;
71 if context.get_global_property_value(T::new()).is_some() {
72 return Err(IxaError::IxaError(format!("Duplicate property {name}")));
73 }
74 context.set_global_property_value(T::new(), val)?;
75 Ok(())
76 },
77 ),
78 getter: Box::new(|context: &Context| -> Result<Option<String>, IxaError> {
79 let value = context.get_global_property_value(T::new());
80 match value {
81 Some(val) => Ok(Some(serde_json::to_string(val)?)),
82 None => Ok(None),
83 }
84 }),
85 }),
86 )
87 .inspect(|_| panic!("Duplicate global property {}", name));
88}
89
90fn get_global_property_accessor(name: &str) -> Option<Arc<PropertyAccessors>> {
91 let properties = GLOBAL_PROPERTIES.lock().unwrap();
92 let tmp = properties.borrow();
93 tmp.get(name).map(Arc::clone)
94}
95
96#[macro_export]
101macro_rules! define_global_property {
102 ($global_property:ident, $value:ty, $validate: expr) => {
103 #[derive(Copy, Clone)]
104 pub struct $global_property;
105
106 impl $crate::global_properties::GlobalProperty for $global_property {
107 type Value = $value;
108
109 fn new() -> Self {
110 $global_property
111 }
112
113 fn validate(val: &$value) -> Result<(), $crate::error::IxaError> {
114 $validate(val)
115 }
116 }
117
118 $crate::paste::paste! {
119 #[$crate::ctor::ctor]
120 fn [<$global_property:snake _register>]() {
121 let module = module_path!();
122 let mut name = module.split("::").next().unwrap().to_string();
123 name += ".";
124 name += stringify!($global_property);
125 $crate::global_properties::add_global_property::<$global_property>(&name);
126 }
127 }
128 };
129
130 ($global_property: ident, $value: ty) => {
131 define_global_property!($global_property, $value, |_| { Ok(()) });
132 };
133}
134
135pub trait GlobalProperty: Any {
139 type Value: Any; fn new() -> Self;
142 #[allow(clippy::missing_errors_doc)]
143 fn validate(value: &Self::Value) -> Result<(), IxaError>;
145}
146
147pub use define_global_property;
148
149struct GlobalPropertiesDataContainer {
150 global_property_container: HashMap<TypeId, Box<dyn Any>>,
151}
152
153crate::context::define_data_plugin!(
154 GlobalPropertiesPlugin,
155 GlobalPropertiesDataContainer,
156 GlobalPropertiesDataContainer {
157 global_property_container: HashMap::default(),
158 }
159);
160
161pub trait ContextGlobalPropertiesExt {
162 fn set_global_property_value<T: GlobalProperty + 'static>(
167 &mut self,
168 property: T,
169 value: T::Value,
170 ) -> Result<(), IxaError>;
171
172 fn get_global_property_value<T: GlobalProperty + 'static>(
174 &self,
175 _property: T,
176 ) -> Option<&T::Value>;
177
178 fn list_registered_global_properties(&self) -> Vec<String>;
179
180 fn get_serialized_value_by_string(&self, name: &str) -> Result<Option<String>, IxaError>;
186
187 fn load_parameters_from_json<T: 'static + Debug + DeserializeOwned>(
195 &mut self,
196 file_path: &Path,
197 ) -> Result<T, IxaError>;
198
199 fn load_global_properties(&mut self, file_name: &Path) -> Result<(), IxaError>;
221}
222
223impl GlobalPropertiesDataContainer {
224 fn set_global_property_value<T: GlobalProperty + 'static>(
225 &mut self,
226 _property: &T,
227 value: T::Value,
228 ) -> Result<(), IxaError> {
229 match self.global_property_container.entry(TypeId::of::<T>()) {
230 Entry::Vacant(entry) => {
231 entry.insert(Box::new(value));
232 Ok(())
233 }
234 Entry::Occupied(_) => Err(IxaError::from("Entry already exists")),
238 }
239 }
240
241 #[must_use]
242 fn get_global_property_value<T: GlobalProperty + 'static>(&self) -> Option<&T::Value> {
243 let data_container = self.global_property_container.get(&TypeId::of::<T>());
244
245 match data_container {
246 Some(property) => Some(property.downcast_ref::<T::Value>().unwrap()),
247 None => None,
248 }
249 }
250}
251
252impl ContextGlobalPropertiesExt for Context {
253 fn set_global_property_value<T: GlobalProperty + 'static>(
254 &mut self,
255 property: T,
256 value: T::Value,
257 ) -> Result<(), IxaError> {
258 T::validate(&value)?;
259 let data_container = self.get_data_container_mut(GlobalPropertiesPlugin);
260 data_container.set_global_property_value(&property, value)
261 }
262
263 #[allow(unused_variables)]
264 fn get_global_property_value<T: GlobalProperty + 'static>(
265 &self,
266 _property: T,
267 ) -> Option<&T::Value> {
268 if let Some(data_container) = self.get_data_container(GlobalPropertiesPlugin) {
269 data_container.get_global_property_value::<T>()
270 } else {
271 None
272 }
273 }
274
275 fn list_registered_global_properties(&self) -> Vec<String> {
276 let properties = GLOBAL_PROPERTIES.lock().unwrap();
277 let tmp = properties.borrow();
278 tmp.keys().cloned().collect()
279 }
280
281 fn get_serialized_value_by_string(&self, name: &str) -> Result<Option<String>, IxaError> {
282 let accessor = get_global_property_accessor(name);
283 match accessor {
284 Some(accessor) => (accessor.getter)(self),
285 None => Err(IxaError::from(format!("No global property: {name}"))),
286 }
287 }
288
289 fn load_parameters_from_json<T: 'static + Debug + DeserializeOwned>(
290 &mut self,
291 file_name: &Path,
292 ) -> Result<T, IxaError> {
293 trace!("Loading parameters from JSON: {file_name:?}");
294 let config_file = fs::File::open(file_name)?;
295 let reader = BufReader::new(config_file);
296 let config = serde_json::from_reader(reader)?;
297 Ok(config)
298 }
299
300 fn load_global_properties(&mut self, file_name: &Path) -> Result<(), IxaError> {
301 trace!("Loading global properties from {file_name:?}");
302 let config_file = fs::File::open(file_name)?;
303 let reader = BufReader::new(config_file);
304 let val: serde_json::Map<String, serde_json::Value> = serde_json::from_reader(reader)?;
305
306 for (k, v) in val {
307 if let Some(accessor) = get_global_property_accessor(&k) {
308 (accessor.setter)(self, &k, v)?;
309 } else {
310 return Err(IxaError::from(format!("No global property: {k}")));
311 }
312 }
313
314 Ok(())
315 }
316}
317
318#[cfg(test)]
319mod test {
320 use super::*;
321 use crate::context::Context;
322 use crate::error::IxaError;
323 use serde::{Deserialize, Serialize};
324 use std::path::PathBuf;
325 use tempfile::tempdir;
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 = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
429 .join("tests/data/global_properties_test1.json");
430 context.load_global_properties(&path).unwrap();
431 let p1 = context.get_global_property_value(Property1).unwrap();
432 assert_eq!(p1.field_int, 1);
433 assert_eq!(p1.field_str, "test");
434 let p2 = context.get_global_property_value(Property2).unwrap();
435 assert_eq!(p2.field_int, 2);
436 }
437
438 #[test]
439 fn read_unknown_property() {
440 let mut context = Context::new();
441 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
442 .join("tests/data/global_properties_missing.json");
443 match context.load_global_properties(&path) {
444 Err(IxaError::IxaError(msg)) => {
445 assert_eq!(msg, "No global property: ixa.PropertyUnknown");
446 }
447 _ => panic!("Unexpected error type"),
448 }
449 }
450
451 #[test]
452 fn read_malformed_property() {
453 let mut context = Context::new();
454 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
455 .join("tests/data/global_properties_malformed.json");
456 let error = context.load_global_properties(&path);
457 println!("Error {error:?}");
458 match error {
459 Err(IxaError::JsonError(_)) => {}
460 _ => panic!("Unexpected error type"),
461 }
462 }
463
464 #[test]
465 fn read_duplicate_property() {
466 let mut context = Context::new();
467 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
468 .join("tests/data/global_properties_test1.json");
469 context.load_global_properties(&path).unwrap();
470 let error = context.load_global_properties(&path);
471 match error {
472 Err(IxaError::IxaError(_)) => {}
473 _ => panic!("Unexpected error type"),
474 }
475 }
476
477 #[derive(Serialize, Deserialize)]
478 pub struct Property3Type {
479 field_int: u32,
480 }
481 define_global_property!(Property3, Property3Type, |v: &Property3Type| {
482 match v.field_int {
483 0 => Ok(()),
484 _ => Err(IxaError::IxaError(format!(
485 "Illegal value for `field_int`: {}",
486 v.field_int
487 ))),
488 }
489 });
490
491 #[test]
492 fn validate_property_set_success() {
493 let mut context = Context::new();
494 context
495 .set_global_property_value(Property3, Property3Type { field_int: 0 })
496 .unwrap();
497 }
498
499 #[test]
500 fn validate_property_set_failure() {
501 let mut context = Context::new();
502 assert!(matches!(
503 context.set_global_property_value(Property3, Property3Type { field_int: 1 }),
504 Err(IxaError::IxaError(_))
505 ));
506 }
507
508 #[test]
509 fn validate_property_load_success() {
510 let mut context = Context::new();
511 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
512 .join("tests/data/global_properties_valid.json");
513 context.load_global_properties(&path).unwrap();
514 }
515
516 #[test]
517 fn validate_property_load_failure() {
518 let mut context = Context::new();
519 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
520 .join("tests/data/global_properties_invalid.json");
521 assert!(matches!(
522 context.load_global_properties(&path),
523 Err(IxaError::IxaError(_))
524 ));
525 }
526
527 #[test]
528 fn list_registered_global_properties() {
529 let context = Context::new();
530 let properties = context.list_registered_global_properties();
531 assert!(properties.contains(&"ixa.DiseaseParams".to_string()));
532 }
533
534 #[test]
535 fn get_serialized_value_by_string() {
536 let mut context = Context::new();
537 context
538 .set_global_property_value(
539 DiseaseParams,
540 ParamType {
541 days: 10,
542 diseases: 2,
543 },
544 )
545 .unwrap();
546 let serialized = context
547 .get_serialized_value_by_string("ixa.DiseaseParams")
548 .unwrap();
549 assert_eq!(serialized, Some("{\"days\":10,\"diseases\":2}".to_string()));
550 }
551}