kind_config/
lib.rs

1use std::collections::HashMap;
2use std::fmt;
3
4/**
5 * Enum (variant) whose kind is either `bool`, `f64`, `f64`, or `String`.
6 * These are the types of values allowed in a `Form`.
7 */
8#[derive(Clone)]
9pub enum Value {
10    B(bool),
11    I(i64),
12    F(f64),
13    S(String),
14}
15
16impl Value {
17    /**
18     * Determines whether this value and another are of the same kind.
19     */
20    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/**
153 * A value and an about string. This is the value type of the HashMap used in a
154 * Form.
155 */
156#[derive(Clone)]
157pub struct Parameter {
158    pub value: Value,
159    pub about: String,
160    pub frozen: bool,
161}
162
163/**
164 * A configuration data structure that is kind-checked at runtime. Items are
165 * declared using the `item` member function, after which their value can be
166 * updated but their kind (bool, int, float, string) cannot change.
167 */
168pub struct Form {
169    parameter_map: HashMap<String, Parameter>,
170}
171
172// ============================================================================
173impl Default for Form {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179impl Form {
180    /**
181     * Creates a blank form.
182     */
183    pub fn new() -> Form {
184        Form {
185            parameter_map: HashMap::new(),
186        }
187    }
188
189    /**
190     * Declares a new config item. Any item already declared with that name is
191     * replaced.
192     *
193     * # Arguments
194     *
195     * * `key`     - The name of the config item
196     * * `default` - The default value
197     * * `about`   - A description of the item for use in user reporting
198     */
199    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    /**
212     * Merges in the contents of a string-value map, and freeze any of those
213     * items which are named in the given vector of keys to be frozen.
214     *
215     * # Arguments
216     *
217     * * `items` - A map of values to update the map with
218     * * `to_freeze` - A vector of keys to freeze, if the key is in `items`
219     */
220    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    /**
235     * Merges in the contents of a string-value map. The result is an error if
236     * any of the new keys have not already been declared in the form, or if
237     * they were declared as a different type.
238     *
239     * # Arguments
240     *
241     * * `items` - A map of values to update the map with
242     */
243    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    /**
261     * Merges in the contents of a string-string map. The result is an error
262     * if any of the new keys have not already been declared in the form, or
263     * if any of the value strings do not parse to the declared type.
264     *
265     * # Arguments
266     *
267     * * `dict` - A map of string to update the map with
268     */
269    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    /**
275     * Merges in a sequence of "key=value" pairs. The result is an error if
276     * any of the new keys have not already been declared in the form, or if
277     * any of the value strings do not parse to the declared type.
278     *
279     * # Arguments
280     *
281     * * `args` - Iterator of string to update the map with
282     *
283     * # Example
284     * ```
285     * # let base = kind_config::Form::new();
286     * let form = base.merge_string_args(std::env::args().skip(1)).unwrap();
287     * ```
288     */
289    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    /**
305     * Freezes a parameter with the given name, if it exists, or otherwise
306     * panic.
307     */
308    pub fn freeze(mut self, key: &str) -> Self {
309        self.parameter_map.get_mut(key).unwrap().frozen = true;
310        self
311    }
312
313    /**
314     * Returns a hash map of the (key, value) items, stripping out the about
315     * messages. If the HDF5 feature is enabled, the result can be written
316     * directly to an HDF5 group via io::write_to_hdf5.
317     */
318    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(), &parameter.value))
329    }
330
331    /**
332     * Returns the number of items.
333     */
334    pub fn len(&self) -> usize {
335        self.parameter_map.len()
336    }
337
338    /**
339     * Returns whether the map is empty.
340     */
341    pub fn is_empty(&self) -> bool {
342        self.parameter_map.is_empty()
343    }
344
345    /**
346     * Returns a vector of the keys in this map, sorted alphabetically.
347     */
348    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    /**
355     * Gets an item from the form. Panics if the item was not declared.
356     *
357     * # Arguments
358     *
359     * * `key` - The key to get
360     *
361     * # Example
362     * ```
363     * # let form = kind_config::Form::new().item("counter", 0, "");
364     * let x: i64 = form.get("counter").into(); // fails unless "counter" is declared has kind i64
365     * ```
366     */
367    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// ============================================================================
522#[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}