1use indexmap::IndexMap;
2use serde_json::{Map, Value};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Default, PartialEq)]
7pub struct OrderedOptions {
8 values: IndexMap<String, Value>,
9}
10
11impl OrderedOptions {
12 #[must_use]
14 pub fn new() -> Self {
15 Self::default()
16 }
17
18 #[must_use]
20 pub fn get(&self, key: &str) -> Option<&Value> {
21 get_path(&self.values, key)
22 }
23
24 pub fn set(&mut self, key: impl Into<String>, value: Value) {
26 set_path(&mut self.values, &key.into(), value);
27 }
28
29 #[must_use]
31 pub fn merge(&self, other: &Self) -> Self {
32 let mut merged = self.clone();
33 for (key, value) in &other.values {
34 match merged.values.get_mut(key) {
35 Some(existing) => merge_value(existing, value),
36 None => {
37 merged.values.insert(key.clone(), value.clone());
38 }
39 }
40 }
41 merged
42 }
43
44 #[must_use]
46 pub fn to_hash(&self) -> HashMap<String, Value> {
47 self.values
48 .iter()
49 .map(|(key, value)| (key.clone(), value.clone()))
50 .collect()
51 }
52}
53
54fn get_path<'a>(root: &'a IndexMap<String, Value>, key: &str) -> Option<&'a Value> {
55 let mut segments = key.split('.').filter(|segment| !segment.is_empty());
56 let first = segments.next()?;
57 let mut current = root.get(first)?;
58
59 for segment in segments {
60 current = current.as_object()?.get(segment)?;
61 }
62
63 Some(current)
64}
65
66fn set_path(root: &mut IndexMap<String, Value>, key: &str, value: Value) {
67 let parts: Vec<&str> = key
68 .split('.')
69 .filter(|segment| !segment.is_empty())
70 .collect();
71 if parts.is_empty() {
72 return;
73 }
74
75 if parts.len() == 1 {
76 root.insert(parts[0].to_owned(), value);
77 return;
78 }
79
80 let mut current = root
81 .entry(parts[0].to_owned())
82 .or_insert_with(|| Value::Object(Map::new()));
83
84 for part in &parts[1..parts.len() - 1] {
85 match current {
86 Value::Object(map) => {
87 current = map
88 .entry((*part).to_owned())
89 .or_insert_with(|| Value::Object(Map::new()));
90 }
91 _ => {
92 *current = Value::Object(Map::new());
93 if let Value::Object(map) = current {
94 current = map
95 .entry((*part).to_owned())
96 .or_insert_with(|| Value::Object(Map::new()));
97 }
98 }
99 }
100 }
101
102 if !current.is_object() {
103 *current = Value::Object(Map::new());
104 }
105
106 if let Value::Object(map) = current {
107 map.insert(parts[parts.len() - 1].to_owned(), value);
108 }
109}
110
111fn merge_value(existing: &mut Value, incoming: &Value) {
112 match (existing, incoming) {
113 (Value::Object(existing), Value::Object(incoming)) => {
114 for (key, value) in incoming {
115 match existing.get_mut(key) {
116 Some(existing_value) => merge_value(existing_value, value),
117 None => {
118 existing.insert(key.clone(), value.clone());
119 }
120 }
121 }
122 }
123 (existing, incoming) => *existing = incoming.clone(),
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::OrderedOptions;
130 use serde_json::json;
131
132 #[test]
133 fn default_starts_empty() {
134 let options = OrderedOptions::default();
135 assert_eq!(options.get("missing"), None);
136 }
137
138 #[test]
139 fn set_and_get_round_trip_top_level_values() {
140 let mut options = OrderedOptions::new();
141 options.set("host", json!("localhost"));
142
143 assert_eq!(options.get("host"), Some(&json!("localhost")));
144 }
145
146 #[test]
147 fn set_creates_nested_objects_for_dotted_paths() {
148 let mut options = OrderedOptions::new();
149 options.set("database.host", json!("localhost"));
150
151 assert_eq!(options.get("database.host"), Some(&json!("localhost")));
152 }
153
154 #[test]
155 fn later_set_overrides_existing_values() {
156 let mut options = OrderedOptions::new();
157 options.set("database.host", json!("localhost"));
158 options.set("database.host", json!("db.internal"));
159
160 assert_eq!(options.get("database.host"), Some(&json!("db.internal")));
161 }
162
163 #[test]
164 fn get_returns_none_for_missing_nested_keys() {
165 let mut options = OrderedOptions::new();
166 options.set("database.host", json!("localhost"));
167
168 assert_eq!(options.get("database.port"), None);
169 }
170
171 #[test]
172 fn get_treats_empty_segments_like_missing_separators() {
173 let mut options = OrderedOptions::new();
174 options.set("..database..host..", json!("localhost"));
175
176 assert_eq!(options.get("database.host"), Some(&json!("localhost")));
177 assert_eq!(options.get("..database..host.."), Some(&json!("localhost")));
178 }
179
180 #[test]
181 fn get_returns_none_for_empty_key() {
182 let options = OrderedOptions::new();
183
184 assert_eq!(options.get(""), None);
185 assert_eq!(options.get("..."), None);
186 }
187
188 #[test]
189 fn set_ignores_empty_path_segments_only_input() {
190 let mut options = OrderedOptions::new();
191 options.set("...", json!("ignored"));
192
193 assert_eq!(options.get(""), None);
194 assert_eq!(options.to_hash(), std::collections::HashMap::new());
195 }
196
197 #[test]
198 fn set_replaces_scalar_intermediates_when_descending_into_nested_paths() {
199 let mut options = OrderedOptions::new();
200 options.set("database", json!("sqlite"));
201 options.set("database.host", json!("localhost"));
202
203 assert_eq!(
204 options.get("database"),
205 Some(&json!({ "host": "localhost" }))
206 );
207 assert_eq!(options.get("database.host"), Some(&json!("localhost")));
208 }
209
210 #[test]
211 fn merge_overrides_existing_scalar_values() {
212 let mut first = OrderedOptions::new();
213 first.set("host", json!("localhost"));
214 let mut second = OrderedOptions::new();
215 second.set("host", json!("db.internal"));
216
217 let merged = first.merge(&second);
218
219 assert_eq!(merged.get("host"), Some(&json!("db.internal")));
220 }
221
222 #[test]
223 fn merge_recursively_combines_nested_objects() {
224 let mut first = OrderedOptions::new();
225 first.set("database.host", json!("localhost"));
226 let mut second = OrderedOptions::new();
227 second.set("database.port", json!(5432));
228
229 let merged = first.merge(&second);
230
231 assert_eq!(merged.get("database.host"), Some(&json!("localhost")));
232 assert_eq!(merged.get("database.port"), Some(&json!(5432)));
233 }
234
235 #[test]
236 fn merge_prefers_incoming_nested_values() {
237 let mut first = OrderedOptions::new();
238 first.set("database.host", json!("localhost"));
239 let mut second = OrderedOptions::new();
240 second.set("database.host", json!("db.internal"));
241
242 let merged = first.merge(&second);
243
244 assert_eq!(merged.get("database.host"), Some(&json!("db.internal")));
245 }
246
247 #[test]
248 fn merge_preserves_existing_top_level_order_when_appending_new_keys() {
249 let mut first = OrderedOptions::new();
250 first.set("host", json!("localhost"));
251 first.set("adapter", json!("sqlite"));
252
253 let mut second = OrderedOptions::new();
254 second.set("pool", json!(5));
255
256 let merged = first.merge(&second);
257 let keys = merged.values.keys().map(String::as_str).collect::<Vec<_>>();
258
259 assert_eq!(keys, vec!["host", "adapter", "pool"]);
260 }
261
262 #[test]
263 fn merge_does_not_mutate_nested_inputs() {
264 let mut first = OrderedOptions::new();
265 first.set("database.host", json!("localhost"));
266
267 let mut second = OrderedOptions::new();
268 second.set("database.port", json!(5432));
269
270 let merged = first.merge(&second);
271
272 assert_eq!(merged.get("database.host"), Some(&json!("localhost")));
273 assert_eq!(merged.get("database.port"), Some(&json!(5432)));
274 assert_eq!(first.get("database.port"), None);
275 assert_eq!(second.get("database.host"), None);
276 }
277
278 #[test]
279 fn merge_replaces_scalar_with_nested_object_from_incoming_options() {
280 let mut first = OrderedOptions::new();
281 first.set("database", json!("sqlite"));
282
283 let mut second = OrderedOptions::new();
284 second.set("database.host", json!("localhost"));
285
286 let merged = first.merge(&second);
287
288 assert_eq!(merged.get("database.host"), Some(&json!("localhost")));
289 }
290
291 #[test]
292 fn merge_with_empty_options_returns_equal_copy() {
293 let mut options = OrderedOptions::new();
294 options.set("database.host", json!("localhost"));
295
296 let merged = options.merge(&OrderedOptions::new());
297
298 assert_eq!(merged, options);
299 }
300
301 #[test]
302 fn to_hash_clones_the_top_level_values() {
303 let mut options = OrderedOptions::new();
304 options.set("database.host", json!("localhost"));
305
306 let mut hash = options.to_hash();
307 hash.insert(String::from("database"), json!("changed"));
308
309 assert_eq!(options.get("database.host"), Some(&json!("localhost")));
310 }
311
312 #[test]
313 fn to_hash_clones_nested_values() {
314 let mut options = OrderedOptions::new();
315 options.set("database.host", json!("localhost"));
316
317 let mut hash = options.to_hash();
318 hash.entry(String::from("database")).and_modify(|value| {
319 value
320 .as_object_mut()
321 .expect("database should be an object")
322 .insert(String::from("host"), json!("db.internal"));
323 });
324
325 assert_eq!(options.get("database.host"), Some(&json!("localhost")));
326 }
327}