renvy/
merge.rs

1use std::collections::BTreeMap;
2
3/// Denotes the type for elements that serve as keys in settings files.
4///
5/// This type is an alias for keys, e.g. the part of a line of a settings file
6/// in front of the first `=`. Even though this is just an alias to `String`
7/// for now, this provides better type safety and lets the compiler know what
8/// you intend to do with a particular variable. This type is used in the
9/// complex type [`Settings`].
10///
11/// For an example how to use `Key` please refer to the [example section of `Settings`](crate::Settings#how-to-use-settings).
12pub type Key = String;
13
14/// Denotes the type for elements that serve as values in settings files.
15///
16/// This type is an alias for optional values, e.g. the part of a line of a settings
17/// file after the first `=`. It's an `Option` because a settings line might end
18/// with the first `=` which is when the value of that line would be `None`. As soon
19/// as there is any contents after the first `=`, the value will be `Some(X)`, where `X`
20/// represents that data.
21///
22/// For an example how to use `Value` please refer to the [example section of `Settings`](crate::Settings#how-to-use-settings).
23pub type Value = Option<String>;
24
25/// Denotes a set of settings as a simple sorted map of Key-Value pairs.
26///
27/// Each entry in this map consists of a key of type [`Key`] and a value of type [`Option<Value>`].
28/// For more information about why values are wrapped in [`Option`] please refer to the
29/// documentation of the type [`Value`].
30///
31/// ## How to use `Settings`
32///
33/// Since `Settings` is just an alias for [`BTreeMap`], you can construct them fairly easily from an array of tuples:
34///
35/// ```
36/// // Create a new Settings object with 1 key-value pair "url=https://example.com"
37/// let my_settings_one = renvy::Settings::from([("url".into(), Some("https://example.com".into()))]);
38/// assert_eq!(my_settings_one.get("url").unwrap(), &Some("https://example.com".into()));
39///
40/// // Create a new Settings object with 1 key-value pair where the value is empty "url="
41/// let my_settings_one = renvy::Settings::from([("url".into(), None)]);
42/// assert_eq!(my_settings_one.get("url").unwrap(), &None);
43/// ```
44///
45/// Besides that, [`crate::deserialize()`] will also return an instance of `Settings`:
46///
47/// ```
48/// // Settings objects will also be returned from renvy::deserialize()
49/// let my_settings_deserialized = renvy::deserialize("url=https://example.com".into());
50/// assert_eq!(my_settings_deserialized.get("url".into()).unwrap(), &Some("https://example.com".into()));
51/// ```
52pub type Settings = BTreeMap<Key, Value>;
53
54/// Merges two instances of [`Settings`] together so that the following rules are satisfied:
55/// - all key-value pairs on `defaults` that are missing on `settings` will be added
56/// - existing key-value pairs of `settings` retain their value
57/// - if the parameter `clean` receives `Some(true)`, then any key-value pair on `settings`
58///   which is missing from `defaults` will be removed
59///
60/// ## Examples
61///
62/// Default behaviour when `clean` is `None`. This is the same behaviour like passing
63/// `Some(false)` explicitly.
64///
65/// ```
66/// // "ssl" exists in both objects, it's "true" here
67/// let settings = renvy::Settings::from([
68///     ("url".into(), Some(String::from("https://example.com"))),
69///     ("ssl".into(), Some(String::from("true")))
70/// ]);
71///
72/// // "ssl" is "false" here
73/// let defaults = renvy::Settings::from([
74///     ("port".into(), None),
75///     ("ssl".into(), Some(String::from("false")))
76/// ]);
77///
78/// let merged = renvy::merge(settings, defaults, None);
79///
80/// // "ssl" remains "true", "port" is added, and "url" is left intact
81/// assert_eq!(merged.get("url".into()).unwrap(), &Some(String::from("https://example.com")));
82/// assert_eq!(merged.get("ssl".into()).unwrap(), &Some(String::from("true")));
83/// assert_eq!(merged.get("port".into()).unwrap(), &None);
84/// ```
85///
86/// Behaviour when `clean` is disabled with `Some(false)`: Extra keys in `settings`
87/// remain untouched. This is the default behaviour that is also applied when `clean`
88/// is empty (`None`).
89///
90/// ```
91/// // "ssl" exists in both objects, it's "true" here
92/// let settings = renvy::Settings::from([
93///     ("url".into(), Some(String::from("https://example.com"))),
94///     ("ssl".into(), Some(String::from("true")))
95/// ]);
96///
97/// // "ssl" is "false" here
98/// let defaults = renvy::Settings::from([
99///     ("port".into(), None),
100///     ("ssl".into(), Some(String::from("false")))
101/// ]);
102///
103/// let merged = renvy::merge(settings, defaults, Some(false));
104///
105/// // "ssl" remains "true", "port" is added, and "url" is left intact
106/// assert_eq!(merged.get("url".into()).unwrap(), &Some(String::from("https://example.com")));
107/// assert_eq!(merged.get("ssl".into()).unwrap(), &Some(String::from("true")));
108/// assert_eq!(merged.get("port".into()).unwrap(), &None);
109/// ```
110///
111/// Behaviour when `clean` is enabled with `Some(true)`: Extra keys in `settings`
112/// are being removed so that only keys that exist in `defaults` remain in `settings`.
113///
114/// ```
115/// // "ssl" exists in both objects, it's "true" here
116/// // "url" exists only in "settings".
117/// let settings = renvy::Settings::from([
118///     ("url".into(), Some(String::from("https://example.com"))),
119///     ("ssl".into(), Some(String::from("true")))
120/// ]);
121///
122/// // "ssl" is "false" here
123/// let defaults = renvy::Settings::from([
124///     ("port".into(), None),
125///     ("ssl".into(), Some(String::from("false")))
126/// ]);
127///
128/// let merged = renvy::merge(settings, defaults, Some(true));
129///
130/// // "ssl" remains "true", "port" is added, and "url" is removed
131/// assert_eq!(merged.get("url".into()), None);
132/// assert_eq!(merged.get("ssl".into()).unwrap(), &Some(String::from("true")));
133/// assert_eq!(merged.get("port".into()).unwrap(), &None);
134/// ```
135///
136pub fn merge(settings: Settings, defaults: Settings, clean: Option<bool>) -> Settings {
137    let mut result: Settings = settings;
138    let clean = if let Some(x) = clean { x } else { false };
139
140    if clean {
141        result.retain(|key, _| defaults.contains_key(key));
142    }
143
144    for default in defaults {
145        let entry = result.entry(default.0);
146        entry.or_insert(default.1);
147    }
148
149    result
150}
151
152#[cfg(test)]
153mod test {
154    use crate::merge;
155
156    #[test]
157    fn merge_adds_new_defaults() {
158        let settings =
159            merge::Settings::from([("domain".into(), Some("https://benjaminsattler.net".into()))]);
160
161        let defaults = merge::Settings::from([("port".into(), Some("433".into()))]);
162
163        let merged = merge::merge(settings, defaults, None);
164
165        assert!(merged.get("port").is_some());
166        assert_eq!(merged.get("port").unwrap(), &Some(String::from("433")));
167    }
168
169    #[test]
170    fn merge_keeps_existing_settings_with_defaults() {
171        let settings =
172            merge::Settings::from([("domain".into(), Some("https://benjaminsattler.net".into()))]);
173
174        let defaults = merge::Settings::from([("domain".into(), Some("https://example".into()))]);
175
176        let merged = merge::merge(settings, defaults, None);
177
178        assert!(merged.get("domain").is_some());
179        assert_eq!(
180            merged.get("domain").unwrap(),
181            &Some(String::from("https://benjaminsattler.net"))
182        );
183    }
184
185    #[test]
186    fn merge_keeps_settings_without_defaults_if_cleaning_is_default() {
187        let settings =
188            merge::Settings::from([("domain".into(), Some("https://benjaminsattler.net".into()))]);
189
190        let defaults = merge::Settings::from([("port".into(), Some("433".into()))]);
191
192        let merged = merge::merge(settings, defaults, None);
193
194        assert!(merged.get("domain").is_some());
195        assert_eq!(
196            merged.get("domain").unwrap(),
197            &Some(String::from("https://benjaminsattler.net"))
198        );
199    }
200
201    #[test]
202    fn merge_keeps_settings_without_defaults_if_not_cleaning() {
203        let settings =
204            merge::Settings::from([("domain".into(), Some("https://benjaminsattler.net".into()))]);
205
206        let defaults = merge::Settings::from([("port".into(), Some("433".into()))]);
207
208        let merged = merge::merge(settings, defaults, Some(false));
209
210        assert!(merged.get("domain").is_some());
211        assert_eq!(
212            merged.get("domain").unwrap(),
213            &Some(String::from("https://benjaminsattler.net"))
214        );
215    }
216
217    #[test]
218    fn merge_discards_settings_without_defaults_if_cleaning() {
219        let settings =
220            merge::Settings::from([("domain".into(), Some("https://benjaminsattler.net".into()))]);
221
222        let defaults = merge::Settings::from([("port".into(), Some("433".into()))]);
223
224        let merged = merge::merge(settings, defaults, Some(true));
225
226        assert!(merged.get("domain").is_none());
227    }
228}