1use crate::context::Context;
19use crate::error::IxaError;
20use crate::{define_data_plugin, trace, HashMap, HashMapExt, PluginContext};
21use serde::de::DeserializeOwned;
22use std::any::{Any, TypeId};
23use std::cell::RefCell;
24use std::collections::hash_map::Entry;
25use std::fmt::Debug;
26use std::fs;
27use std::io::BufReader;
28use std::path::Path;
29use std::sync::Arc;
30use std::sync::LazyLock;
31use std::sync::Mutex;
32
33type PropertySetterFn =
34 dyn Fn(&mut Context, &str, serde_json::Value) -> Result<(), IxaError> + Send + Sync;
35
36type PropertyGetterFn = dyn Fn(&Context) -> Result<Option<String>, IxaError> + Send + Sync;
37
38pub struct PropertyAccessors {
39 setter: Box<PropertySetterFn>,
40 getter: Box<PropertyGetterFn>,
41}
42
43#[allow(clippy::type_complexity)]
44#[doc(hidden)]
51pub static GLOBAL_PROPERTIES: LazyLock<Mutex<RefCell<HashMap<String, Arc<PropertyAccessors>>>>> =
52 LazyLock::new(|| Mutex::new(RefCell::new(HashMap::new())));
53
54#[allow(clippy::missing_panics_doc)]
55pub fn add_global_property<T: GlobalProperty>(name: &str)
56where
57 for<'de> <T as GlobalProperty>::Value: serde::Deserialize<'de> + serde::Serialize,
58{
59 trace!("Adding global property {name}");
60 let properties = GLOBAL_PROPERTIES.lock().unwrap();
61 properties
62 .borrow_mut()
63 .insert(
64 name.to_string(),
65 Arc::new(PropertyAccessors {
66 setter: Box::new(
67 |context: &mut Context, name, value| -> Result<(), IxaError> {
68 let val: T::Value = serde_json::from_value(value)?;
69 T::validate(&val)?;
70 if context.get_global_property_value(T::new()).is_some() {
71 return Err(IxaError::IxaError(format!("Duplicate property {name}")));
72 }
73 context.set_global_property_value(T::new(), val)?;
74 Ok(())
75 },
76 ),
77 getter: Box::new(|context: &Context| -> Result<Option<String>, IxaError> {
78 let value = context.get_global_property_value(T::new());
79 match value {
80 Some(val) => Ok(Some(serde_json::to_string(val)?)),
81 None => Ok(None),
82 }
83 }),
84 }),
85 )
86 .inspect(|_| panic!("Duplicate global property {}", name));
87}
88
89fn get_global_property_accessor(name: &str) -> Option<Arc<PropertyAccessors>> {
90 let properties = GLOBAL_PROPERTIES.lock().unwrap();
91 let tmp = properties.borrow();
92 tmp.get(name).map(Arc::clone)
93}
94
95#[macro_export]
100macro_rules! define_global_property {
101 ($global_property:ident, $value:ty, $validate: expr) => {
102 #[derive(Copy, Clone)]
103 pub struct $global_property;
104
105 impl $crate::global_properties::GlobalProperty for $global_property {
106 type Value = $value;
107
108 fn new() -> Self {
109 $global_property
110 }
111
112 fn validate(val: &$value) -> Result<(), $crate::error::IxaError> {
113 $validate(val)
114 }
115 }
116
117 $crate::paste::paste! {
118 $crate::ctor::declarative::ctor!{
119 #[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
131 ($global_property: ident, $value: ty) => {
132 define_global_property!($global_property, $value, |_| { Ok(()) });
133 };
134}
135
136pub trait GlobalProperty: Any {
140 type Value: Any; fn new() -> Self;
143 #[allow(clippy::missing_errors_doc)]
144 fn validate(value: &Self::Value) -> Result<(), IxaError>;
146}
147
148pub use define_global_property;
149
150struct GlobalPropertiesDataContainer {
151 global_property_container: HashMap<TypeId, Box<dyn Any>>,
152}
153
154define_data_plugin!(
155 GlobalPropertiesPlugin,
156 GlobalPropertiesDataContainer,
157 GlobalPropertiesDataContainer {
158 global_property_container: HashMap::default(),
159 }
160);
161
162impl GlobalPropertiesDataContainer {
163 fn set_global_property_value<T: GlobalProperty + 'static>(
164 &mut self,
165 _property: &T,
166 value: T::Value,
167 ) -> Result<(), IxaError> {
168 match self.global_property_container.entry(TypeId::of::<T>()) {
169 Entry::Vacant(entry) => {
170 entry.insert(Box::new(value));
171 Ok(())
172 }
173 Entry::Occupied(_) => Err(IxaError::from("Entry already exists")),
177 }
178 }
179
180 #[must_use]
181 fn get_global_property_value<T: GlobalProperty + 'static>(&self) -> Option<&T::Value> {
182 let data_container = self.global_property_container.get(&TypeId::of::<T>());
183
184 match data_container {
185 Some(property) => Some(property.downcast_ref::<T::Value>().unwrap()),
186 None => None,
187 }
188 }
189}
190
191pub trait ContextGlobalPropertiesExt: PluginContext {
192 fn set_global_property_value<T: GlobalProperty + 'static>(
197 &mut self,
198 property: T,
199 value: T::Value,
200 ) -> Result<(), IxaError> {
201 T::validate(&value)?;
202 let data_container = self.get_data_mut(GlobalPropertiesPlugin);
203 data_container.set_global_property_value(&property, value)
204 }
205
206 #[allow(unused_variables)]
208 fn get_global_property_value<T: GlobalProperty + 'static>(
209 &self,
210 _property: T,
211 ) -> Option<&T::Value> {
212 self.get_data(GlobalPropertiesPlugin)
213 .get_global_property_value::<T>()
214 }
215
216 fn list_registered_global_properties(&self) -> Vec<String> {
217 let properties = GLOBAL_PROPERTIES.lock().unwrap();
218 let tmp = properties.borrow();
219 tmp.keys().cloned().collect()
220 }
221
222 fn get_serialized_value_by_string(&self, name: &str) -> Result<Option<String>, IxaError>;
228
229 fn load_parameters_from_json<T: 'static + Debug + DeserializeOwned>(
237 &mut self,
238 file_name: &Path,
239 ) -> Result<T, IxaError> {
240 trace!("Loading parameters from JSON: {file_name:?}");
241 let config_file = fs::File::open(file_name)?;
242 let reader = BufReader::new(config_file);
243 let config = serde_json::from_reader(reader)?;
244 Ok(config)
245 }
246
247 fn load_global_properties(&mut self, file_name: &Path) -> Result<(), IxaError>;
269}
270impl ContextGlobalPropertiesExt for Context {
271 fn get_serialized_value_by_string(&self, name: &str) -> Result<Option<String>, IxaError> {
272 let accessor = get_global_property_accessor(name);
273 match accessor {
274 Some(accessor) => (accessor.getter)(self),
275 None => Err(IxaError::from(format!("No global property: {name}"))),
276 }
277 }
278
279 fn load_global_properties(&mut self, file_name: &Path) -> Result<(), IxaError> {
280 trace!("Loading global properties from {file_name:?}");
281 let config_file = fs::File::open(file_name)?;
282 let reader = BufReader::new(config_file);
283 let val: serde_json::Map<String, serde_json::Value> = serde_json::from_reader(reader)?;
284
285 for (k, v) in val {
286 if let Some(accessor) = get_global_property_accessor(&k) {
287 (accessor.setter)(self, &k, v)?;
288 } else {
289 return Err(IxaError::from(format!("No global property: {k}")));
290 }
291 }
292
293 Ok(())
294 }
295}
296
297#[cfg(test)]
298mod test {
299 use super::*;
300 use crate::context::Context;
301 use crate::error::IxaError;
302 use serde::{Deserialize, Serialize};
303 use std::path::PathBuf;
304 use tempfile::tempdir;
305 #[derive(Serialize, Deserialize, Debug, Clone)]
306 pub struct ParamType {
307 pub days: usize,
308 pub diseases: usize,
309 }
310
311 define_global_property!(DiseaseParams, ParamType);
312
313 #[test]
314 fn set_get_global_property() {
315 let params: ParamType = ParamType {
316 days: 10,
317 diseases: 2,
318 };
319 let params2: ParamType = ParamType {
320 days: 11,
321 diseases: 3,
322 };
323
324 let mut context = Context::new();
325
326 context
328 .set_global_property_value(DiseaseParams, params.clone())
329 .unwrap();
330 let global_params = context
331 .get_global_property_value(DiseaseParams)
332 .unwrap()
333 .clone();
334 assert_eq!(global_params.days, params.days);
335 assert_eq!(global_params.diseases, params.diseases);
336
337 assert!(context
339 .set_global_property_value(DiseaseParams, params2.clone())
340 .is_err());
341
342 let global_params = context
344 .get_global_property_value(DiseaseParams)
345 .unwrap()
346 .clone();
347 assert_eq!(global_params.days, params.days);
348 assert_eq!(global_params.diseases, params.diseases);
349 }
350
351 #[test]
352 fn get_global_propert_missing() {
353 let context = Context::new();
354 let global_params = context.get_global_property_value(DiseaseParams);
355 assert!(global_params.is_none());
356 }
357
358 #[test]
359 fn set_parameters() {
360 let mut context = Context::new();
361 let temp_dir = tempdir().unwrap();
362 let config_path = PathBuf::from(&temp_dir.path());
363 let file_name = "test.json";
364 let file_path = config_path.join(file_name);
365 let config = fs::File::create(config_path.join(file_name)).unwrap();
366
367 let params: ParamType = ParamType {
368 days: 10,
369 diseases: 2,
370 };
371
372 define_global_property!(Parameters, ParamType);
373
374 let _ = serde_json::to_writer(config, ¶ms);
375 let params_json = context
376 .load_parameters_from_json::<ParamType>(&file_path)
377 .unwrap();
378
379 context
380 .set_global_property_value(Parameters, params_json)
381 .unwrap();
382
383 let params_read = context
384 .get_global_property_value(Parameters)
385 .unwrap()
386 .clone();
387 assert_eq!(params_read.days, params.days);
388 assert_eq!(params_read.diseases, params.diseases);
389 }
390
391 #[derive(Serialize, Deserialize)]
392 pub struct Property1Type {
393 field_int: u32,
394 field_str: String,
395 }
396 define_global_property!(Property1, Property1Type);
397
398 #[derive(Serialize, Deserialize)]
399 pub struct Property2Type {
400 field_int: u32,
401 }
402 define_global_property!(Property2, Property2Type);
403
404 #[test]
405 fn read_global_properties() {
406 let mut context = Context::new();
407 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
408 .join("tests/data/global_properties_test1.json");
409 context.load_global_properties(&path).unwrap();
410 let p1 = context.get_global_property_value(Property1).unwrap();
411 assert_eq!(p1.field_int, 1);
412 assert_eq!(p1.field_str, "test");
413 let p2 = context.get_global_property_value(Property2).unwrap();
414 assert_eq!(p2.field_int, 2);
415 }
416
417 #[test]
418 fn read_unknown_property() {
419 let mut context = Context::new();
420 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
421 .join("tests/data/global_properties_missing.json");
422 match context.load_global_properties(&path) {
423 Err(IxaError::IxaError(msg)) => {
424 assert_eq!(msg, "No global property: ixa.PropertyUnknown");
425 }
426 _ => panic!("Unexpected error type"),
427 }
428 }
429
430 #[test]
431 fn read_malformed_property() {
432 let mut context = Context::new();
433 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
434 .join("tests/data/global_properties_malformed.json");
435 let error = context.load_global_properties(&path);
436 println!("Error {error:?}");
437 match error {
438 Err(IxaError::JsonError(_)) => {}
439 _ => panic!("Unexpected error type"),
440 }
441 }
442
443 #[test]
444 fn read_duplicate_property() {
445 let mut context = Context::new();
446 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
447 .join("tests/data/global_properties_test1.json");
448 context.load_global_properties(&path).unwrap();
449 let error = context.load_global_properties(&path);
450 match error {
451 Err(IxaError::IxaError(_)) => {}
452 _ => panic!("Unexpected error type"),
453 }
454 }
455
456 #[derive(Serialize, Deserialize)]
457 pub struct Property3Type {
458 field_int: u32,
459 }
460 define_global_property!(Property3, Property3Type, |v: &Property3Type| {
461 match v.field_int {
462 0 => Ok(()),
463 _ => Err(IxaError::IxaError(format!(
464 "Illegal value for `field_int`: {}",
465 v.field_int
466 ))),
467 }
468 });
469
470 #[test]
471 fn validate_property_set_success() {
472 let mut context = Context::new();
473 context
474 .set_global_property_value(Property3, Property3Type { field_int: 0 })
475 .unwrap();
476 }
477
478 #[test]
479 fn validate_property_set_failure() {
480 let mut context = Context::new();
481 assert!(matches!(
482 context.set_global_property_value(Property3, Property3Type { field_int: 1 }),
483 Err(IxaError::IxaError(_))
484 ));
485 }
486
487 #[test]
488 fn validate_property_load_success() {
489 let mut context = Context::new();
490 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
491 .join("tests/data/global_properties_valid.json");
492 context.load_global_properties(&path).unwrap();
493 }
494
495 #[test]
496 fn validate_property_load_failure() {
497 let mut context = Context::new();
498 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
499 .join("tests/data/global_properties_invalid.json");
500 assert!(matches!(
501 context.load_global_properties(&path),
502 Err(IxaError::IxaError(_))
503 ));
504 }
505
506 #[test]
507 fn list_registered_global_properties() {
508 let context = Context::new();
509 let properties = context.list_registered_global_properties();
510 assert!(properties.contains(&"ixa.DiseaseParams".to_string()));
511 }
512
513 #[test]
514 fn get_serialized_value_by_string() {
515 let mut context = Context::new();
516 context
517 .set_global_property_value(
518 DiseaseParams,
519 ParamType {
520 days: 10,
521 diseases: 2,
522 },
523 )
524 .unwrap();
525 let serialized = context
526 .get_serialized_value_by_string("ixa.DiseaseParams")
527 .unwrap();
528 assert_eq!(serialized, Some("{\"days\":10,\"diseases\":2}".to_string()));
529 }
530}