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}