tpnote_lib/
config_value.rs

1//! Provides a newtype for `toml::map::Map<String, Value>)` with methods
2//! to merge (incomplete) configuration data from different sources (files).
3
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7use toml::Value;
8
9use crate::error::LibCfgError;
10
11/// This decides until what depth arrays are merged into the default
12/// configuration. Tables are always merged. Deeper arrays replace the default
13/// configuration. For our configuration this means, that `scheme` is merged and
14/// all other arrays are replaced.
15pub(crate) const CONFIG_FILE_MERGE_DEPTH: isize = 2;
16
17/// A newtype holding configuration data.
18#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
19pub struct CfgVal(toml::map::Map<String, Value>);
20
21/// This API deals with configuration values.
22///
23impl CfgVal {
24    /// Constructor returning an empty map.
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// Append key, value pairs from other to `self`.
30    ///
31    /// ```rust
32    /// use tpnote_lib::config_value::CfgVal;
33    /// use std::str::FromStr;
34    ///
35    /// let toml1 = "\
36    /// [arg_default]
37    /// scheme = 'zettel'
38    /// ";
39    ///
40    /// let toml2 = "\
41    /// [base_scheme]
42    /// name = 'some name'
43    /// ";
44    ///
45    /// let mut cfg1 = CfgVal::from_str(toml1).unwrap();
46    /// let cfg2 = CfgVal::from_str(toml2).unwrap();
47    ///
48    /// let expected = CfgVal::from_str("\
49    /// [arg_default]
50    /// scheme = 'zettel'
51    /// [base_scheme]
52    /// name = 'some name'
53    /// ").unwrap();
54    ///
55    /// // Run test
56    /// cfg1.extend(cfg2);
57    ///
58    /// assert_eq!(cfg1, expected);
59    ///
60    #[inline]
61    pub fn extend(&mut self, other: Self) {
62        self.0.extend(other.0);
63    }
64
65    #[inline]
66    pub fn insert(&mut self, key: String, val: Value) {
67        self.0.insert(key, val); //
68    }
69
70    #[inline]
71    /// Merges configuration values from `other` into `self`
72    /// and returns the result. The top level element is a set of key and value
73    /// pairs (map). If one of its values is a `Value::Array`, then the
74    /// corresponding array from `other` is appended.
75    /// Otherwise the corresponding `other` value replaces the `self` value.
76    /// Deeper nested `Value::Array`s are never appended but always replaced
77    /// (`CONFIG_FILE_MERGE_PEPTH=2`).
78    /// Append key, value pairs from other to `self`.
79    ///
80    /// ```rust
81    /// use tpnote_lib::config_value::CfgVal;
82    /// use std::str::FromStr;
83    ///
84    /// let toml1 = "\
85    /// version = '1.0.0'
86    /// [[scheme]]
87    /// name = 'default'
88    /// ";
89    /// let toml2 = "\
90    /// version = '2.0.0'
91    /// [[scheme]]
92    /// name = 'zettel'
93    /// ";
94    ///
95    /// let mut cfg1 = CfgVal::from_str(toml1).unwrap();
96    /// let cfg2 = CfgVal::from_str(toml2).unwrap();
97    ///
98    /// let expected = CfgVal::from_str("\
99    /// version = '2.0.0'
100    /// [[scheme]]
101    /// name = 'default'
102    /// [[scheme]]
103    /// name = 'zettel'
104    /// ").unwrap();
105    ///
106    /// // Run test
107    /// let res = cfg1.merge(cfg2);
108    ///
109    /// assert_eq!(res, expected);
110    ///
111    pub fn merge(self, other: Self) -> Self {
112        let left = Value::Table(self.0);
113        let right = Value::Table(other.0);
114        let res = Self::merge_toml_values(left, right, CONFIG_FILE_MERGE_DEPTH);
115        // Invariant: when left and right are `Value::Table`, then `res`
116        // must be a `Value::Table` also.
117        if let Value::Table(map) = res {
118            Self(map)
119        } else {
120            unreachable!()
121        }
122    }
123
124    /// Merges configuration values from the right-hand side into the
125    /// left-hand side and returns the result. The top level element is usually
126    /// a `toml::Value::Table`. The table is a set of key and value pairs.
127    /// The values here can be compound data types, i.e. `Value::Table` or
128    /// `Value::Array`.
129    /// `merge_depth` controls whether a top-level array in the TOML document
130    /// is appended to instead of overridden. This is useful for TOML documents
131    /// that have a top-level arrays (`merge_depth=2`) like `[[scheme]]` in
132    /// `tpnote.toml`. For top level arrays, one usually wants to append the
133    /// right-hand array to the left-hand array instead of just replacing the
134    /// left-hand array with the right-hand array. If you set `merge_depth=0`,
135    /// all arrays whatever level they have, are always overridden by the
136    /// right-hand side.
137    pub(crate) fn merge_toml_values(
138        left: toml::Value,
139        right: toml::Value,
140        merge_depth: isize,
141    ) -> toml::Value {
142        use toml::Value;
143
144        fn get_name(v: &Value) -> Option<&str> {
145            v.get("name").and_then(Value::as_str)
146        }
147
148        match (left, right) {
149            (Value::Array(mut left_items), Value::Array(right_items)) => {
150                // The top-level arrays should be merged but nested arrays
151                // should act as overrides. For the `tpnote.toml` config,
152                // this means that you can specify a sub-set of schemes in
153                // an overriding `tpnote.toml` but that nested arrays like
154                // `scheme.tmpl.fm_var_localization` are replaced instead
155                // of merged.
156                if merge_depth > 0 {
157                    left_items.reserve(right_items.len());
158                    for rvalue in right_items {
159                        let lvalue = get_name(&rvalue)
160                            .and_then(|rname| {
161                                left_items.iter().position(|v| get_name(v) == Some(rname))
162                            })
163                            .map(|lpos| left_items.remove(lpos));
164                        let mvalue = match lvalue {
165                            Some(lvalue) => {
166                                Self::merge_toml_values(lvalue, rvalue, merge_depth - 1)
167                            }
168                            None => rvalue,
169                        };
170                        left_items.push(mvalue);
171                    }
172                    Value::Array(left_items)
173                } else {
174                    Value::Array(right_items)
175                }
176            }
177            (Value::Table(mut left_map), Value::Table(right_map)) => {
178                if merge_depth > -10 {
179                    for (rname, rvalue) in right_map {
180                        match left_map.remove(&rname) {
181                            Some(lvalue) => {
182                                let merged_value =
183                                    Self::merge_toml_values(lvalue, rvalue, merge_depth - 1);
184                                left_map.insert(rname, merged_value);
185                            }
186                            None => {
187                                left_map.insert(rname, rvalue);
188                            }
189                        }
190                    }
191                    Value::Table(left_map)
192                } else {
193                    Value::Table(right_map)
194                }
195            }
196            (_, value) => value,
197        }
198    }
199
200    /// Convert to `toml::Value`.
201    ///
202    /// ```rust
203    /// use tpnote_lib::config_value::CfgVal;
204    /// use std::str::FromStr;
205    ///
206    /// let toml1 = "\
207    /// version = 1
208    /// [[scheme]]
209    /// name = 'default'
210    /// ";
211    ///
212    /// let cfg1 = CfgVal::from_str(toml1).unwrap();
213    ///
214    /// let expected: toml::Value = toml::from_str(toml1).unwrap();
215    ///
216    /// // Run test
217    /// let res = cfg1.to_value();
218    ///
219    /// assert_eq!(res, expected);
220    ///
221    pub fn to_value(self) -> toml::Value {
222        Value::Table(self.0)
223    }
224}
225
226impl FromStr for CfgVal {
227    type Err = LibCfgError;
228
229    /// Constructor taking a text to deserialize.
230    /// Throws an error if the deserialized root element is not a
231    /// `Value::Table`.
232    fn from_str(s: &str) -> Result<Self, Self::Err> {
233        let v = toml::from_str(s)?;
234        if let Value::Table(map) = v {
235            Ok(Self(map))
236        } else {
237            Err(LibCfgError::CfgValInputIsNotTable)
238        }
239    }
240}
241
242impl From<CfgVal> for toml::Value {
243    fn from(cfg_val: CfgVal) -> Self {
244        cfg_val.to_value()
245    }
246}