1use std::any::{Any, TypeId};
19use std::cell::RefCell;
20use std::collections::hash_map::Entry;
21use std::fmt::Debug;
22use std::fs;
23use std::io::BufReader;
24use std::path::Path;
25use std::sync::{Arc, LazyLock, Mutex};
26
27use serde::de::DeserializeOwned;
28
29use crate::context::Context;
30use crate::error::IxaError;
31use crate::{define_data_plugin, trace, ContextBase, HashMap, HashMapExt};
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
95pub trait GlobalProperty: Any {
99 type Value: Any; fn new() -> Self;
102 #[allow(clippy::missing_errors_doc)]
103 fn validate(value: &Self::Value) -> Result<(), IxaError>;
105}
106
107struct GlobalPropertiesDataContainer {
108 global_property_container: HashMap<TypeId, Box<dyn Any>>,
109}
110
111define_data_plugin!(
112 GlobalPropertiesPlugin,
113 GlobalPropertiesDataContainer,
114 GlobalPropertiesDataContainer {
115 global_property_container: HashMap::default(),
116 }
117);
118
119impl GlobalPropertiesDataContainer {
120 fn set_global_property_value<T: GlobalProperty + 'static>(
121 &mut self,
122 _property: &T,
123 value: T::Value,
124 ) -> Result<(), IxaError> {
125 match self.global_property_container.entry(TypeId::of::<T>()) {
126 Entry::Vacant(entry) => {
127 entry.insert(Box::new(value));
128 Ok(())
129 }
130 Entry::Occupied(_) => Err(IxaError::from("Entry already exists")),
134 }
135 }
136
137 #[must_use]
138 fn get_global_property_value<T: GlobalProperty + 'static>(&self) -> Option<&T::Value> {
139 let data_container = self.global_property_container.get(&TypeId::of::<T>());
140
141 match data_container {
142 Some(property) => Some(property.downcast_ref::<T::Value>().unwrap()),
143 None => None,
144 }
145 }
146}
147
148pub trait ContextGlobalPropertiesExt: ContextBase {
149 fn set_global_property_value<T: GlobalProperty + 'static>(
154 &mut self,
155 property: T,
156 value: T::Value,
157 ) -> Result<(), IxaError> {
158 T::validate(&value)?;
159 let data_container = self.get_data_mut(GlobalPropertiesPlugin);
160 data_container.set_global_property_value(&property, value)
161 }
162
163 #[allow(unused_variables)]
165 fn get_global_property_value<T: GlobalProperty + 'static>(
166 &self,
167 _property: T,
168 ) -> Option<&T::Value> {
169 self.get_data(GlobalPropertiesPlugin)
170 .get_global_property_value::<T>()
171 }
172
173 fn list_registered_global_properties(&self) -> Vec<String> {
174 let properties = GLOBAL_PROPERTIES.lock().unwrap();
175 let tmp = properties.borrow();
176 tmp.keys().cloned().collect()
177 }
178
179 fn get_serialized_value_by_string(&self, name: &str) -> Result<Option<String>, IxaError>;
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>;
226}
227impl ContextGlobalPropertiesExt for Context {
228 fn get_serialized_value_by_string(&self, name: &str) -> Result<Option<String>, IxaError> {
229 let accessor = get_global_property_accessor(name);
230 match accessor {
231 Some(accessor) => (accessor.getter)(self),
232 None => Err(IxaError::from(format!("No global property: {name}"))),
233 }
234 }
235
236 fn load_global_properties(&mut self, file_name: &Path) -> Result<(), IxaError> {
237 trace!("Loading global properties from {file_name:?}");
238 let config_file = fs::File::open(file_name)?;
239 let reader = BufReader::new(config_file);
240 let val: serde_json::Map<String, serde_json::Value> = serde_json::from_reader(reader)?;
241
242 for (k, v) in val {
243 if let Some(accessor) = get_global_property_accessor(&k) {
244 (accessor.setter)(self, &k, v)?;
245 } else {
246 return Err(IxaError::from(format!("No global property: {k}")));
247 }
248 }
249
250 Ok(())
251 }
252}
253
254#[cfg(test)]
255mod test {
256 use std::path::PathBuf;
257
258 use serde::{Deserialize, Serialize};
259 use tempfile::tempdir;
260
261 use super::*;
262 use crate::context::Context;
263 use crate::define_global_property;
264 use crate::error::IxaError;
265
266 #[derive(Serialize, Deserialize, Debug, Clone)]
267 pub struct ParamType {
268 pub days: usize,
269 pub diseases: usize,
270 }
271
272 define_global_property!(DiseaseParams, ParamType);
273
274 #[test]
275 fn set_get_global_property() {
276 let params: ParamType = ParamType {
277 days: 10,
278 diseases: 2,
279 };
280 let params2: ParamType = ParamType {
281 days: 11,
282 diseases: 3,
283 };
284
285 let mut context = Context::new();
286
287 context
289 .set_global_property_value(DiseaseParams, params.clone())
290 .unwrap();
291 let global_params = context
292 .get_global_property_value(DiseaseParams)
293 .unwrap()
294 .clone();
295 assert_eq!(global_params.days, params.days);
296 assert_eq!(global_params.diseases, params.diseases);
297
298 assert!(context
300 .set_global_property_value(DiseaseParams, params2.clone())
301 .is_err());
302
303 let global_params = context
305 .get_global_property_value(DiseaseParams)
306 .unwrap()
307 .clone();
308 assert_eq!(global_params.days, params.days);
309 assert_eq!(global_params.diseases, params.diseases);
310 }
311
312 #[test]
313 fn get_global_propert_missing() {
314 let context = Context::new();
315 let global_params = context.get_global_property_value(DiseaseParams);
316 assert!(global_params.is_none());
317 }
318
319 #[test]
320 fn set_parameters() {
321 let mut context = Context::new();
322 let temp_dir = tempdir().unwrap();
323 let config_path = PathBuf::from(&temp_dir.path());
324 let file_name = "test.json";
325 let file_path = config_path.join(file_name);
326 let config = fs::File::create(config_path.join(file_name)).unwrap();
327
328 let params: ParamType = ParamType {
329 days: 10,
330 diseases: 2,
331 };
332
333 define_global_property!(Parameters, ParamType);
334
335 let _ = serde_json::to_writer(config, ¶ms);
336 let params_json = context
337 .load_parameters_from_json::<ParamType>(&file_path)
338 .unwrap();
339
340 context
341 .set_global_property_value(Parameters, params_json)
342 .unwrap();
343
344 let params_read = context
345 .get_global_property_value(Parameters)
346 .unwrap()
347 .clone();
348 assert_eq!(params_read.days, params.days);
349 assert_eq!(params_read.diseases, params.diseases);
350 }
351
352 #[derive(Serialize, Deserialize)]
353 pub struct Property1Type {
354 field_int: u32,
355 field_str: String,
356 }
357 define_global_property!(Property1, Property1Type);
358
359 #[derive(Serialize, Deserialize)]
360 pub struct Property2Type {
361 field_int: u32,
362 }
363 define_global_property!(Property2, Property2Type);
364
365 #[test]
366 fn read_global_properties() {
367 let mut context = Context::new();
368 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
369 .join("tests/data/global_properties_test1.json");
370 context.load_global_properties(&path).unwrap();
371 let p1 = context.get_global_property_value(Property1).unwrap();
372 assert_eq!(p1.field_int, 1);
373 assert_eq!(p1.field_str, "test");
374 let p2 = context.get_global_property_value(Property2).unwrap();
375 assert_eq!(p2.field_int, 2);
376 }
377
378 #[test]
379 fn read_unknown_property() {
380 let mut context = Context::new();
381 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
382 .join("tests/data/global_properties_missing.json");
383 match context.load_global_properties(&path) {
384 Err(IxaError::IxaError(msg)) => {
385 assert_eq!(msg, "No global property: ixa.PropertyUnknown");
386 }
387 _ => panic!("Unexpected error type"),
388 }
389 }
390
391 #[test]
392 fn read_malformed_property() {
393 let mut context = Context::new();
394 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
395 .join("tests/data/global_properties_malformed.json");
396 let error = context.load_global_properties(&path);
397 println!("Error {error:?}");
398 match error {
399 Err(IxaError::JsonError(_)) => {}
400 _ => panic!("Unexpected error type"),
401 }
402 }
403
404 #[test]
405 fn read_duplicate_property() {
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 error = context.load_global_properties(&path);
411 match error {
412 Err(IxaError::IxaError(_)) => {}
413 _ => panic!("Unexpected error type"),
414 }
415 }
416
417 #[derive(Serialize, Deserialize)]
418 pub struct Property3Type {
419 field_int: u32,
420 }
421 define_global_property!(Property3, Property3Type, |v: &Property3Type| {
422 match v.field_int {
423 0 => Ok(()),
424 _ => Err(IxaError::IxaError(format!(
425 "Illegal value for `field_int`: {}",
426 v.field_int
427 ))),
428 }
429 });
430
431 #[test]
432 fn validate_property_set_success() {
433 let mut context = Context::new();
434 context
435 .set_global_property_value(Property3, Property3Type { field_int: 0 })
436 .unwrap();
437 }
438
439 #[test]
440 fn validate_property_set_failure() {
441 let mut context = Context::new();
442 assert!(matches!(
443 context.set_global_property_value(Property3, Property3Type { field_int: 1 }),
444 Err(IxaError::IxaError(_))
445 ));
446 }
447
448 #[test]
449 fn validate_property_load_success() {
450 let mut context = Context::new();
451 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
452 .join("tests/data/global_properties_valid.json");
453 context.load_global_properties(&path).unwrap();
454 }
455
456 #[test]
457 fn validate_property_load_failure() {
458 let mut context = Context::new();
459 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
460 .join("tests/data/global_properties_invalid.json");
461 assert!(matches!(
462 context.load_global_properties(&path),
463 Err(IxaError::IxaError(_))
464 ));
465 }
466
467 #[test]
468 fn list_registered_global_properties() {
469 let context = Context::new();
470 let properties = context.list_registered_global_properties();
471 assert!(properties.contains(&"ixa.DiseaseParams".to_string()));
472 }
473
474 #[test]
475 fn get_serialized_value_by_string() {
476 let mut context = Context::new();
477 context
478 .set_global_property_value(
479 DiseaseParams,
480 ParamType {
481 days: 10,
482 diseases: 2,
483 },
484 )
485 .unwrap();
486 let serialized = context
487 .get_serialized_value_by_string("ixa.DiseaseParams")
488 .unwrap();
489 assert_eq!(serialized, Some("{\"days\":10,\"diseases\":2}".to_string()));
490 }
491}