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}