config_it/shared/
archive.rs

1use std::{cell::Cell, mem::take};
2
3use compact_str::{CompactString, ToCompactString};
4use serde::{ser::SerializeMap, Deserialize, Serialize};
5
6#[cfg(not(feature = "indexmap"))]
7type Map<T, V> = std::collections::BTreeMap<T, V>;
8
9#[cfg(feature = "indexmap")]
10type Map<T, V> = indexmap::IndexMap<T, V>;
11
12/// Defines rules for serializing category names within the [`Archive`].
13///
14/// When an [`Archive`] is serialized, it manifests as a map of key-value pairs, where the key is a
15/// string, and the value is a plain object. Due to the indistinguishable nature of category keys
16/// from value object keys, a unique naming convention for categories is necessary.
17///
18/// The default naming convention prefixes category names with `~`. For instance, a category named
19/// `hello` is serialized as `~hello`.
20///
21/// Users can customize this rule for both serialization and deserialization processes by invoking
22/// the [`with_category_rule`] function.
23pub enum CategoryRule<'a> {
24    /// Categories are signified by prefixing their names with the specified token.
25    Prefix(&'a str),
26
27    /// Categories are signified by appending their names with the specified token.
28    Suffix(&'a str),
29
30    /// Categories are signified by wrapping their names between the specified start and end tokens.
31    Wrap(&'a str, &'a str),
32}
33
34thread_local! {
35    static CATEGORY_RULE: Cell<CategoryRule<'static>> = Cell::new(Default::default());
36}
37
38/// Temporarily overrides the category serialization rule while serializing or deserializing a map.
39///
40/// This function allows for a custom `CategoryRule` to be specified for the duration of the
41/// provided closure, `f`. This is especially useful when there's a need to diverge from the default
42/// category naming convention during a specific serialization or deserialization task.
43///
44/// # Safety
45///
46/// The implementation uses thread-local storage (`CATEGORY_RULE`) to store the custom rule. The
47/// original rule or default value is safely restored upon function exit, ensuring consistent
48/// behavior across different invocations. Moreover, this function is panic-safe, meaning if a panic
49/// occurs within the closure, the rule is still guaranteed to be restored before the panic
50/// propagates.
51///
52/// # Arguments
53///
54/// * `rule`: The custom category rule to apply.
55/// * `f`: A closure representing the serialization or deserialization task where the custom rule
56///   should be used.
57///
58/// # Panics
59///
60/// This function will propagate any panics that occur within the provided closure.
61pub fn with_category_rule(rule: CategoryRule, f: impl FnOnce() + std::panic::UnwindSafe) {
62    CATEGORY_RULE.with(|x| unsafe {
63        // SAFETY: Temporarily override lifetime as &'static; The `x` is guaranteed to be restored
64        //         to its original value on function exit, even if a panic occurs.
65        x.replace(std::mem::transmute(rule));
66
67        let err = std::panic::catch_unwind(|| {
68            f();
69        });
70
71        x.replace(Default::default());
72
73        // Let panic propagate
74        err.unwrap();
75    })
76}
77
78impl<'a> Default for CategoryRule<'a> {
79    fn default() -> Self {
80        Self::Prefix("~")
81    }
82}
83
84impl<'a> CategoryRule<'a> {
85    pub fn is_category(&self, key: &str) -> bool {
86        match self {
87            Self::Prefix(prefix) => key.starts_with(prefix),
88            Self::Suffix(suffix) => key.ends_with(suffix),
89            Self::Wrap(prefix, suffix) => key.starts_with(prefix) && key.ends_with(suffix),
90        }
91    }
92
93    pub fn make_category(&self, key: &str, out_key: &mut CompactString) {
94        out_key.clear();
95
96        match self {
97            CategoryRule::Prefix(tok) => {
98                out_key.push_str(tok);
99                out_key.push_str(key);
100            }
101
102            CategoryRule::Suffix(tok) => {
103                out_key.push_str(key);
104                out_key.push_str(tok);
105            }
106
107            CategoryRule::Wrap(pre, suf) => {
108                out_key.push_str(pre);
109                out_key.push_str(key);
110                out_key.push_str(suf);
111            }
112        }
113    }
114}
115
116/// Represents a hierarchical archive of configuration values and categories.
117///
118/// The `Archive` struct organizes configuration data into a tree-like structure. Each node in this
119/// structure can represent either a configuration category (identified by a path) or an individual
120/// configuration value.
121///
122/// Categories within the archive are uniquely identified by keys that are prefixed with a special
123/// character, typically `~`, though this can be customized using [`with_category_rule`]. Each
124/// category can further contain nested categories and values, allowing for a deeply nested
125/// hierarchical organization of configuration data.
126///
127/// Configuration values within a category are stored as key-value pairs, where the key is a string
128/// representing the configuration name, and the value is its associated JSON representation.
129///
130/// This structure provides a compact and efficient way to represent, serialize, and deserialize
131/// configuration data with support for custom category naming conventions.
132#[derive(Default, Clone, Debug, PartialEq)]
133pub struct Archive {
134    /// Every '~' prefixed keys
135    pub(crate) paths: Map<CompactString, Archive>,
136
137    /// All elements except child path nodes.
138    pub(crate) values: Map<CompactString, serde_json::Value>,
139}
140
141impl Archive {
142    pub fn iter_values(&self) -> impl Iterator<Item = (&str, &serde_json::Value)> {
143        self.values.iter().map(|(k, v)| (k.as_str(), v))
144    }
145
146    pub fn iter_paths(&self) -> impl Iterator<Item = (&str, &Archive)> {
147        self.paths.iter().map(|(k, v)| (k.as_str(), v))
148    }
149
150    pub fn iter_paths_mut(&mut self) -> impl Iterator<Item = (&str, &mut Archive)> {
151        self.paths.iter_mut().map(|(k, v)| (k.as_str(), v))
152    }
153
154    pub fn iter_values_mut(&mut self) -> impl Iterator<Item = (&str, &mut serde_json::Value)> {
155        self.values.iter_mut().map(|(k, v)| (k.as_str(), v))
156    }
157
158    pub fn get_value(&self, key: &str) -> Option<&serde_json::Value> {
159        self.values.get(key)
160    }
161
162    pub fn get_value_mut(&mut self, key: &str) -> Option<&mut serde_json::Value> {
163        self.values.get_mut(key)
164    }
165
166    pub fn get_path(&self, key: &str) -> Option<&Archive> {
167        self.paths.get(key)
168    }
169
170    pub fn get_path_mut(&mut self, key: &str) -> Option<&mut Archive> {
171        self.paths.get_mut(key)
172    }
173
174    pub fn insert_value(&mut self, key: impl ToCompactString, value: serde_json::Value) {
175        self.values.insert(key.to_compact_string(), value);
176    }
177
178    pub fn insert_path(&mut self, key: impl ToCompactString, value: Archive) {
179        self.paths.insert(key.to_compact_string(), value);
180    }
181
182    pub fn remove_value(&mut self, key: &str) -> Option<serde_json::Value> {
183        self.values.remove(key)
184    }
185
186    pub fn remove_path(&mut self, key: &str) -> Option<Archive> {
187        self.paths.remove(key)
188    }
189
190    pub fn clear_values(&mut self) {
191        self.values.clear();
192    }
193
194    pub fn clear_paths(&mut self) {
195        self.paths.clear();
196    }
197
198    pub fn is_empty_values(&self) -> bool {
199        self.values.is_empty()
200    }
201
202    pub fn is_empty_paths(&self) -> bool {
203        self.paths.is_empty()
204    }
205
206    pub fn len_values(&self) -> usize {
207        self.values.len()
208    }
209
210    pub fn len_paths(&self) -> usize {
211        self.paths.len()
212    }
213}
214
215impl Archive {
216    /// Searches for a nested category within the archive using the specified path.
217    ///
218    /// Given a path (as an iterator of strings), this method traverses the archive hierarchically
219    /// to locate the target category.
220    ///
221    /// # Parameters
222    /// * `path`: The path of the target category, represented as an iterable of strings.
223    ///
224    /// # Returns
225    /// An `Option` containing a reference to the found `Archive` (category) if it exists,
226    /// or `None` otherwise.
227    pub fn find_path<'s, 'a, T: AsRef<str> + 'a>(
228        &'s self,
229        path: impl IntoIterator<Item = T>,
230    ) -> Option<&'s Archive> {
231        let iter = path.into_iter();
232        let mut paths = &self.paths;
233        let mut node = None;
234
235        for key in iter {
236            if let Some(next_node) = paths.get(key.as_ref()) {
237                node = Some(next_node);
238                paths = &next_node.paths;
239            } else {
240                return None;
241            }
242        }
243
244        node
245    }
246
247    /// Retrieves a mutable reference to a nested category, creating it if it doesn't exist.
248    ///
249    /// This method is useful for ensuring a category exists at a certain path, creating any
250    /// necessary intermediate categories along the way.
251    ///
252    /// # Parameters
253    /// * `path`: The path of the target category, represented as an iterable of strings.
254    ///
255    /// # Returns
256    /// A mutable reference to the target `Archive` (category).
257    pub fn find_or_create_path_mut<'s, 'a>(
258        &'s mut self,
259        path: impl IntoIterator<Item = &'a str>,
260    ) -> &'s mut Archive {
261        path.into_iter().fold(self, |node, key| node.paths.entry(key.into()).or_default())
262    }
263
264    /// Generates a differential patch between the current and a newer archive.
265    ///
266    /// This method examines the differences between the current archive and a provided newer
267    /// archive. The result is an `Archive` containing only the differences. The method also
268    /// modifies the `newer` archive in place, removing the elements that are part of the patch.
269    ///
270    /// # Parameters
271    /// * `newer`: A mutable reference to the newer version of the archive.
272    ///
273    /// # Returns
274    /// An `Archive` containing only the differences between the current and newer archives.
275    pub fn create_patch(&self, newer: &mut Self) -> Self {
276        let mut patch = Self::default();
277
278        newer.paths.retain(|k, v| {
279            if let Some(base_v) = self.paths.get(k) {
280                let patch_v = base_v.create_patch(v);
281                if !patch_v.is_empty() {
282                    patch.paths.insert(k.clone(), patch_v);
283                    !v.is_empty()
284                } else {
285                    true
286                }
287            } else {
288                patch.paths.insert(k.clone(), take(v));
289                false
290            }
291        });
292
293        newer.values.retain(|k, v| {
294            if let Some(base_v) = self.values.get(k) {
295                if *base_v != *v {
296                    patch.values.insert(k.clone(), take(v));
297                    false
298                } else {
299                    true
300                }
301            } else {
302                patch.values.insert(k.clone(), take(v));
303                false
304            }
305        });
306
307        patch
308    }
309
310    /// Checks if the archive is empty.
311    ///
312    /// An archive is considered empty if it has no categories (paths) and no values.
313    ///
314    /// # Returns
315    /// `true` if the archive is empty, otherwise `false`.
316    pub fn is_empty(&self) -> bool {
317        self.paths.is_empty() && self.values.is_empty()
318    }
319
320    /// Merges data from another archive into the current one.
321    ///
322    /// This method recursively merges categories and replaces values from the other archive into
323    /// the current one. In the case of overlapping categories, it will dive deeper and merge the
324    /// inner values and categories.
325    ///
326    /// # Parameters
327    /// * `other`: The other archive to merge from.
328    pub fn merge_from(&mut self, other: Self) {
329        // Recursively merge p
330        for (k, v) in other.paths {
331            self.paths.entry(k).or_default().merge_from(v);
332        }
333
334        // Value merge is done with simple replace
335        for (k, v) in other.values {
336            self.values.insert(k, v);
337        }
338    }
339
340    /// Merges data from another archive into a clone of the current one and returns the merged
341    /// result.
342    ///
343    /// This method is a combinatory operation that uses `merge_from` under the hood but doesn't
344    /// modify the current archive in place, instead, it returns a new merged archive.
345    ///
346    /// # Parameters
347    /// * `other`: The other archive to merge from.
348    ///
349    /// # Returns
350    /// A new `Archive` which is the result of merging the current archive with the provided one.
351    #[must_use]
352    pub fn merge(mut self, other: Self) -> Self {
353        self.merge_from(other);
354        self
355    }
356}
357
358impl<'a> Deserialize<'a> for Archive {
359    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
360    where
361        D: serde::Deserializer<'a>,
362    {
363        struct PathNodeVisit {
364            build: Archive,
365        }
366
367        impl<'de> serde::de::Visitor<'de> for PathNodeVisit {
368            type Value = Archive;
369
370            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
371                formatter.write_str("Object consist of Tilde(~) prefixed objects or ")
372            }
373
374            fn visit_map<A>(mut self, mut map: A) -> Result<Self::Value, A::Error>
375            where
376                A: serde::de::MapAccess<'de>,
377            {
378                CATEGORY_RULE.with(|rule| {
379                    let rule = rule.take();
380
381                    while let Some(mut key) = map.next_key::<CompactString>()? {
382                        if !key.is_empty() && rule.is_category(&key) {
383                            key.remove(0); // Exclude initial tilde
384
385                            let child: Archive = map.next_value()?;
386                            self.build.paths.insert(key, child);
387                        } else {
388                            let value: serde_json::Value = map.next_value()?;
389                            self.build.values.insert(key, value);
390                        }
391                    }
392
393                    Ok(self.build)
394                })
395            }
396        }
397
398        deserializer.deserialize_map(PathNodeVisit { build: Default::default() })
399    }
400}
401
402impl Serialize for Archive {
403    fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error>
404    where
405        S: serde::Serializer,
406    {
407        let mut map = se.serialize_map(Some(self.paths.len() + self.values.len()))?;
408
409        CATEGORY_RULE.with(|rule| {
410            let rule = rule.take();
411            let mut key_b = CompactString::default();
412
413            for (k, v) in &self.paths {
414                rule.make_category(k, &mut key_b);
415                map.serialize_entry(&key_b, v)?;
416            }
417
418            Ok(())
419        })?;
420
421        for (k, v) in &self.values {
422            debug_assert!(
423                !k.starts_with('~'),
424                "Tilde prefixed key '{k}' for field is not allowed!"
425            );
426
427            map.serialize_entry(k, v)?;
428        }
429
430        map.end()
431    }
432}
433
434#[test]
435#[allow(clippy::approx_constant)]
436fn test_archive_basic() {
437    let src = r#"
438        {
439            "~root_path_1": {
440                "~subpath1": {
441                    "value1": null,
442                    "value2": {},
443                    "~sub-subpath": {}
444                },
445                "~subpath2": {}
446            },
447            "~root_path_2": {
448                "value1": null,
449                "value2": 31.4,
450                "value3": "hoho-haha",
451                "value-obj": {
452                    "~pathlike": 3.141
453                }
454            }
455        }
456    "#;
457
458    let arch: Archive = serde_json::from_str(src).unwrap();
459    assert!(arch.paths.len() == 2);
460
461    let p1 = arch.paths.get("root_path_1").unwrap();
462    assert!(p1.paths.len() == 2);
463    assert!(p1.values.is_empty());
464
465    let sp1 = p1.paths.get("subpath1").unwrap();
466    assert!(sp1.paths.contains_key("sub-subpath"));
467    assert!(sp1.values.len() == 2);
468    assert!(sp1.values.contains_key("value1"));
469    assert!(sp1.values.contains_key("value2"));
470    assert!(sp1.values.get("value1").unwrap().is_null());
471    assert!(sp1.values.get("value2").unwrap().as_object().unwrap().is_empty());
472
473    let p2 = arch.paths.get("root_path_2").unwrap();
474    assert!(p2.paths.is_empty());
475    assert!(p2.values.len() == 4);
476
477    let newer = r#"
478        {
479            "~root_path_1": {
480                "~subpath1": {
481                    "value1": null,
482                    "value2": {
483                        "hello, world!": 3.141
484                    },
485                    "~sub-subpath": {}
486                },
487                "~subpath2": {},
488                "~new_path": {
489                    "valll": 4.44
490                }
491            },
492            "~root_path_2": {
493                "value1": null,
494                "value2": 31.4,
495                "value3": "hoho-haha",
496                "value-obj": {
497                    "~pathlike": 3.141
498                }
499            }
500        }
501    "#;
502    let newer: Archive = serde_json::from_str(newer).unwrap();
503    let mut newer_consume = newer.clone();
504    let patch = Archive::create_patch(&arch, &mut newer_consume);
505
506    let merged = arch.clone().merge(patch.clone());
507    assert_eq!(merged, newer);
508
509    assert!(patch.paths.len() == 1);
510    assert!(patch.paths.contains_key("root_path_1"));
511
512    let val = &patch.find_path(["root_path_1", "subpath1"]).unwrap().values;
513    let val_obj = val.get("value2").unwrap().as_object().unwrap();
514    assert!(val.contains_key("value2"));
515    assert!(val_obj.len() == 1);
516    assert!(val_obj.contains_key("hello, world!"));
517    assert!(val_obj.get("hello, world!") == Some(&serde_json::Value::from(3.141)));
518
519    let val = &patch.find_path(["root_path_1", "new_path"]).unwrap().values;
520    assert!(val.contains_key("valll"));
521    assert!(val.get("valll") == Some(&serde_json::Value::from(4.44)));
522}