1use std::collections::HashMap;
2use std::fmt;
3
4#[derive(Clone)]
9pub enum Value {
10 B(bool),
11 I(i64),
12 F(f64),
13 S(String),
14}
15
16impl Value {
17 pub fn same_kind_as(&self, other: &Value) -> bool {
21 matches!(
22 (&self, &other),
23 (Value::B(_), Value::B(_))
24 | (Value::I(_), Value::I(_))
25 | (Value::F(_), Value::F(_))
26 | (Value::S(_), Value::S(_))
27 )
28 }
29
30 pub fn same_as(&self, other: &Value) -> bool {
31 #[allow(clippy::float_cmp)]
32 match (&self, &other) {
33 (Value::B(a), Value::B(b)) => a == b,
34 (Value::I(a), Value::I(b)) => a == b,
35 (Value::F(a), Value::F(b)) => a == b,
36 (Value::S(a), Value::S(b)) => a == b,
37 _ => false,
38 }
39 }
40
41 pub fn as_str(&self) -> &str {
42 match self {
43 Value::S(s) => &s,
44 _ => panic!(),
45 }
46 }
47}
48
49impl fmt::Display for Value {
50 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51 match self {
52 Value::B(x) => x.fmt(f),
53 Value::I(x) => x.fmt(f),
54 Value::F(x) => x.fmt(f),
55 Value::S(x) => x.fmt(f),
56 }
57 }
58}
59
60impl From<bool> for Value {
61 fn from(a: bool) -> Self {
62 Value::B(a)
63 }
64}
65
66impl From<i64> for Value {
67 fn from(a: i64) -> Self {
68 Value::I(a)
69 }
70}
71
72impl From<f64> for Value {
73 fn from(a: f64) -> Self {
74 Value::F(a)
75 }
76}
77
78impl From<&str> for Value {
79 fn from(a: &str) -> Self {
80 Value::S(a.into())
81 }
82}
83
84impl<'a> From<&'a Value> for bool {
85 fn from(a: &'a Value) -> bool {
86 match a {
87 Value::B(x) => *x,
88 _ => panic!(),
89 }
90 }
91}
92
93impl<'a> From<&'a Value> for i64 {
94 fn from(a: &'a Value) -> i64 {
95 match a {
96 Value::I(x) => *x,
97 _ => panic!(),
98 }
99 }
100}
101
102impl<'a> From<&'a Value> for f64 {
103 fn from(a: &'a Value) -> f64 {
104 match a {
105 Value::F(x) => *x,
106 _ => panic!(),
107 }
108 }
109}
110
111impl<'a> From<&'a Value> for String {
112 fn from(a: &'a Value) -> String {
113 match a {
114 Value::S(x) => x.clone(),
115 _ => panic!(),
116 }
117 }
118}
119
120impl<'a> From<&'a Value> for &'a str {
121 fn from(a: &'a Value) -> &'a str {
122 match a {
123 Value::S(x) => x,
124 _ => panic!(),
125 }
126 }
127}
128
129#[derive(Debug)]
130pub struct ConfigError {
131 key: String,
132 why: String,
133}
134
135impl ConfigError {
136 pub fn new(key: &str, why: &str) -> ConfigError {
137 ConfigError {
138 key: key.into(),
139 why: why.into(),
140 }
141 }
142}
143
144impl fmt::Display for ConfigError {
145 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
146 write!(f, "config key '{}' {}", self.key, self.why)
147 }
148}
149
150impl std::error::Error for ConfigError {}
151
152#[derive(Clone)]
157pub struct Parameter {
158 pub value: Value,
159 pub about: String,
160 pub frozen: bool,
161}
162
163pub struct Form {
169 parameter_map: HashMap<String, Parameter>,
170}
171
172impl Default for Form {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179impl Form {
180 pub fn new() -> Form {
184 Form {
185 parameter_map: HashMap::new(),
186 }
187 }
188
189 pub fn item<T: Into<Value>>(mut self, key: &str, default: T, about: &str) -> Self {
200 self.parameter_map.insert(
201 key.into(),
202 Parameter {
203 value: default.into(),
204 about: about.into(),
205 frozen: false,
206 },
207 );
208 self
209 }
210
211 pub fn merge_value_map_freezing(
221 self,
222 items: &HashMap<String, Value>,
223 to_freeze: &[&str],
224 ) -> Result<Self, ConfigError> {
225 let mut result = self.merge_value_map(items)?;
226 for key in to_freeze {
227 if items.contains_key(*key) {
228 result.parameter_map.get_mut(*key).unwrap().frozen = true;
229 }
230 }
231 Ok(result)
232 }
233
234 pub fn merge_value_map(mut self, items: &HashMap<String, Value>) -> Result<Self, ConfigError> {
244 for (key, new_value) in items {
245 if let Some(item) = self.parameter_map.get_mut(key) {
246 if !item.value.same_kind_as(new_value) {
247 return Err(ConfigError::new(key, "has the wrong type"));
248 } else if item.frozen && !item.value.same_as(new_value) {
249 return Err(ConfigError::new(key, "cannot be modified"));
250 } else {
251 item.value = new_value.clone();
252 }
253 } else {
254 return Err(ConfigError::new(key, "is not a valid key"));
255 }
256 }
257 Ok(self)
258 }
259
260 pub fn merge_string_map(self, dict: &HashMap<String, String>) -> Result<Self, ConfigError> {
270 let items = self.string_map_to_value_map(dict)?;
271 self.merge_value_map(&items)
272 }
273
274 pub fn merge_string_args<T: IntoIterator<Item = U>, U: Into<String>>(
290 self,
291 args: T,
292 ) -> Result<Self, ConfigError> {
293 to_string_map_from_key_val_pairs(args).map(|res| self.merge_string_map(&res))?
294 }
295
296 pub fn merge_string_args_allowing_duplicates<T: IntoIterator<Item = U>, U: Into<String>>(
297 self,
298 args: T,
299 ) -> Result<Self, ConfigError> {
300 to_string_map_from_key_val_pairs_allowing_duplicates(args)
301 .map(|res| self.merge_string_map(&res))?
302 }
303
304 pub fn freeze(mut self, key: &str) -> Self {
309 self.parameter_map.get_mut(key).unwrap().frozen = true;
310 self
311 }
312
313 pub fn value_map(&self) -> HashMap<String, Value> {
319 self.parameter_map
320 .iter()
321 .map(|(key, parameter)| (key.clone(), parameter.value.clone()))
322 .collect()
323 }
324
325 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
326 self.parameter_map
327 .iter()
328 .map(|(key, parameter)| (key.as_str(), ¶meter.value))
329 }
330
331 pub fn len(&self) -> usize {
335 self.parameter_map.len()
336 }
337
338 pub fn is_empty(&self) -> bool {
342 self.parameter_map.is_empty()
343 }
344
345 pub fn sorted_keys(&self) -> Vec<String> {
349 let mut result: Vec<String> = self.parameter_map.keys().map(|x| x.to_string()).collect();
350 result.sort();
351 result
352 }
353
354 pub fn get(&self, key: &str) -> &Value {
368 &self.parameter_map.get(key).unwrap().value
369 }
370
371 pub fn about(&self, key: &str) -> &str {
372 &self.parameter_map.get(key).unwrap().about
373 }
374
375 pub fn is_frozen(&self, key: &str) -> bool {
376 self.parameter_map.get(key).unwrap().frozen
377 }
378
379 fn string_map_to_value_map(
380 &self,
381 dict: &HashMap<String, String>,
382 ) -> Result<HashMap<String, Value>, ConfigError> {
383 use Value::*;
384
385 let mut result = HashMap::new();
386
387 for (k, v) in dict {
388 let parameter = self
389 .parameter_map
390 .get(k)
391 .ok_or_else(|| ConfigError::new(&k, "is not a valid key"))?;
392 let value = match parameter.value {
393 B(_) => v
394 .parse()
395 .map(B)
396 .map_err(|_| ConfigError::new(k, "is a badly formed bool")),
397 I(_) => v
398 .parse()
399 .map(I)
400 .map_err(|_| ConfigError::new(k, "is a badly formed int")),
401 F(_) => v
402 .parse()
403 .map(F)
404 .map_err(|_| ConfigError::new(k, "is a badly formed float")),
405 S(_) => v
406 .parse()
407 .map(S)
408 .map_err(|_| ConfigError::new(k, "is a badly formed string")),
409 }?;
410 result.insert(k.to_string(), value);
411 }
412 Ok(result)
413 }
414}
415
416impl IntoIterator for Form {
417 type Item = <HashMap<String, Parameter> as IntoIterator>::Item;
418 type IntoIter = <HashMap<String, Parameter> as IntoIterator>::IntoIter;
419 fn into_iter(self) -> Self::IntoIter {
420 self.parameter_map.into_iter()
421 }
422}
423
424fn to_string_map_from_key_val_pairs_general<T: IntoIterator<Item = U>, U: Into<String>>(
425 args: T,
426 allow_duplicates: bool,
427) -> Result<HashMap<String, String>, ConfigError> {
428 fn left_and_right_hand_side(a: &str) -> Result<(&str, &str), ConfigError> {
429 let lr: Vec<&str> = a.split('=').collect();
430 if lr.len() != 2 {
431 Err(ConfigError::new(a, "is a badly formed argument"))
432 } else {
433 Ok((lr[0], lr[1]))
434 }
435 }
436 let mut result = HashMap::new();
437 for arg in args {
438 let str_arg: String = arg.into();
439 let (key, value) = left_and_right_hand_side(&str_arg)?;
440 if !allow_duplicates && result.contains_key(key) {
441 return Err(ConfigError::new(key, "duplicate parameter"));
442 }
443 result.insert(key.to_string(), value.to_string());
444 }
445 Ok(result)
446}
447
448pub fn to_string_map_from_key_val_pairs<T: IntoIterator<Item = U>, U: Into<String>>(
449 args: T,
450) -> Result<HashMap<String, String>, ConfigError> {
451 to_string_map_from_key_val_pairs_general(args, false)
452}
453
454pub fn to_string_map_from_key_val_pairs_allowing_duplicates<
455 T: IntoIterator<Item = U>,
456 U: Into<String>,
457>(
458 args: T,
459) -> Result<HashMap<String, String>, ConfigError> {
460 to_string_map_from_key_val_pairs_general(args, true)
461}
462
463#[cfg(feature = "hdf5")]
464pub mod io {
465 use super::*;
466 use hdf5;
467
468 pub fn write_to_hdf5(
469 group: &hdf5::Group,
470 value_map: &HashMap<String, Value>,
471 ) -> Result<(), hdf5::Error> {
472 use hdf5::types::VarLenAscii;
473
474 for (key, value) in value_map {
475 match &value {
476 Value::B(x) => group.new_dataset::<bool>().create(key, ())?.write_scalar(x),
477 Value::I(x) => group.new_dataset::<i64>().create(key, ())?.write_scalar(x),
478 Value::F(x) => group.new_dataset::<f64>().create(key, ())?.write_scalar(x),
479 Value::S(x) => group
480 .new_dataset::<VarLenAscii>()
481 .create(key, ())?
482 .write_scalar(&VarLenAscii::from_ascii(&x).unwrap()),
483 }?;
484 }
485 Ok(())
486 }
487
488 pub fn read_from_hdf5(group: &hdf5::Group) -> Result<HashMap<String, Value>, hdf5::Error> {
489 use hdf5::types::VarLenAscii;
490 let mut values = HashMap::<String, Value>::new();
491
492 for key in group.member_names()? {
493 let dtype = group.dataset(&key)?.dtype()?;
494 let value = if dtype.is::<bool>() {
495 group
496 .dataset(&key)?
497 .read_scalar::<bool>()
498 .map(|x| Value::from(x))
499 } else if dtype.is::<i64>() {
500 group
501 .dataset(&key)?
502 .read_scalar::<i64>()
503 .map(|x| Value::from(x))
504 } else if dtype.is::<f64>() {
505 group
506 .dataset(&key)?
507 .read_scalar::<f64>()
508 .map(|x| Value::from(x))
509 } else {
510 group
511 .dataset(&key)?
512 .read_scalar::<VarLenAscii>()
513 .map(|x| Value::from(x.as_str()))
514 }?;
515 values.insert(key.to_string(), value);
516 }
517 Ok(values)
518 }
519}
520
521#[cfg(test)]
523mod tests {
524 use crate::to_string_map_from_key_val_pairs;
525 use crate::Form;
526 use crate::Value;
527 use std::collections::HashMap;
528
529 fn make_example_form() -> Form {
530 Form::new()
531 .item("num_zones", 5000, "Number of grid cells to use")
532 .item("tfinal", 0.2, "Time at which to stop the simulation")
533 .item("rk_order", 2, "Runge-Kutta time integration order")
534 .item("quiet", false, "Suppress the iteration message")
535 .item("outdir", "data", "Directory where output data is written")
536 }
537
538 #[test]
539 fn can_freeze_parameter() {
540 let form = make_example_form().freeze("num_zones");
541 assert!(form.is_frozen("num_zones"));
542 assert!(!form.is_frozen("outdir"));
543 }
544
545 #[test]
546 fn can_merge_in_command_line_args() {
547 let form = make_example_form()
548 .merge_string_args(std::env::args().skip(1))
549 .unwrap();
550 assert!(i64::from(form.get("num_zones")) == 5000);
551 }
552
553 #[test]
554 fn can_merge_vector_of_args() {
555 let args = to_string_map_from_key_val_pairs(vec!["tfinal=0.4", "rk_order=1", "quiet=true"])
556 .unwrap();
557 let form = make_example_form().merge_string_map(&args).unwrap();
558 assert!(i64::from(form.get("num_zones")) == 5000);
559 assert!(f64::from(form.get("tfinal")) == 0.4);
560 assert!(i64::from(form.get("rk_order")) == 1);
561 assert!(bool::from(form.get("quiet")) == true);
562 }
563
564 #[test]
565 fn can_merge_value_map() {
566 let args: HashMap<String, Value> = vec![
567 ("num_zones".to_string(), Value::from(2000)),
568 ("quiet".to_string(), Value::from(true)),
569 ]
570 .into_iter()
571 .collect();
572
573 let form = make_example_form().merge_value_map(&args).unwrap();
574
575 assert!(i64::from(form.get("num_zones")) == 2000);
576 assert!(f64::from(form.get("tfinal")) == 0.2);
577 assert!(i64::from(form.get("rk_order")) == 2);
578 assert!(bool::from(form.get("quiet")) == true);
579 }
580
581 #[test]
582 fn can_merge_freeze_value_map() {
583 let args: HashMap<String, Value> = vec![
584 ("num_zones".to_string(), Value::from(2000)),
585 ("quiet".to_string(), Value::from(true)),
586 ]
587 .into_iter()
588 .collect();
589
590 let form = make_example_form()
591 .merge_value_map_freezing(&args, &vec!["num_zones", "rk_order"])
592 .unwrap();
593
594 assert!(form.is_frozen("num_zones"));
595 assert!(!form.is_frozen("rk_order"));
596 }
597
598 #[test]
599 #[should_panic]
600 fn to_string_map_fails_with_duplicate_parameter() {
601 to_string_map_from_key_val_pairs(vec!["a=1".to_string(), "a=2".to_string()]).unwrap();
602 }
603
604 #[test]
605 #[should_panic]
606 fn to_string_map_fails_with_badly_formed_parameter() {
607 to_string_map_from_key_val_pairs(vec!["a 2".to_string()]).unwrap();
608 }
609
610 #[test]
611 #[should_panic]
612 fn merge_value_map_fails_with_kind_mismatch() {
613 let args: HashMap<String, Value> = vec![
614 ("num_zones".to_string(), Value::from(3.14)),
615 ("quiet".to_string(), Value::from(true)),
616 ]
617 .into_iter()
618 .collect();
619 make_example_form().merge_value_map(&args).unwrap();
620 }
621
622 #[cfg(feature = "hdf5")]
623 #[cfg(test)]
624 mod io_tests {
625 use super::*;
626
627 #[test]
628 fn can_write_to_hdf5() {
629 let file = hdf5::File::create("test1.h5").unwrap();
630 let form = make_example_form();
631 io::write_to_hdf5(&form.value_map(), &file).unwrap();
632 }
633
634 #[test]
635 fn can_read_from_hdf5() {
636 io::write_to_hdf5(
637 &make_example_form().value_map(),
638 &hdf5::File::create("test2.h5").unwrap(),
639 )
640 .unwrap();
641 let file = hdf5::File::open("test2.h5").unwrap();
642 let value_map = io::read_from_hdf5(&file).unwrap();
643 let form = make_example_form().merge_value_map(&value_map).unwrap();
644 assert_eq!(form.len(), 5);
645 assert_eq!(form.get("num_zones").as_int(), 5000);
646 }
647 }
648}