Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration inheritance

Overlay configuration scopes bottom-up. or_insert in accumulate gives parent-wins semantics — init runs before accumulate, so the parent’s values are already in the map.

#![allow(unused)]
fn main() {
//! Configuration inheritance with overlay/merge.
//! Demonstrates: a fold where the heap IS a config map,
//! and children's configs overlay the parent's defaults.

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;
    use hylic::prelude::*;
    use insta::assert_snapshot;


    /// A configuration scope. Each scope has its own key-value overrides
    /// and child scopes that inherit and can further override.
    #[derive(Clone, Debug)]
    struct ConfigScope {
        name: String,
        overrides: BTreeMap<String, String>,
        children: Vec<ConfigScope>,
    }

    impl ConfigScope {
        fn new(name: &str, overrides: &[(&str, &str)], children: Vec<ConfigScope>) -> Self {
            ConfigScope {
                name: name.into(),
                overrides: overrides.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
                children,
            }
        }
        fn leaf(name: &str, overrides: &[(&str, &str)]) -> Self {
            Self::new(name, overrides, vec![])
        }
    }

    /// Resolved configuration: the merged key-value map for a scope,
    /// collecting all overrides from the scope and its descendants.
    #[derive(Clone, Debug, PartialEq)]
    struct ResolvedConfig {
        scope: String,
        merged: BTreeMap<String, String>,
    }

    #[test]
    fn config_overlay() {
        let root = ConfigScope::new("global", &[
            ("color", "blue"),
            ("font_size", "12"),
            ("theme", "light"),
        ], vec![
            ConfigScope::new("production", &[
                ("theme", "dark"),
                ("debug", "false"),
            ], vec![
                ConfigScope::leaf("production.api", &[
                    ("font_size", "14"),
                    ("rate_limit", "1000"),
                ]),
            ]),
            ConfigScope::leaf("development", &[
                ("debug", "true"),
                ("theme", "light"),
            ]),
        ]);

        let graph: Treeish<ConfigScope> =
            treeish_from(|scope: &ConfigScope| scope.children.as_slice());

        // init seeds the heap with the scope's own overrides; init runs
        // before accumulate, so parent values win — child entries only
        // fill in keys the parent hasn't set.
        let resolve: Fold<ConfigScope, ResolvedConfig, ResolvedConfig> = fold(
            |scope: &ConfigScope| ResolvedConfig {
                scope:  scope.name.clone(),
                merged: scope.overrides.clone(),
            },
            |heap: &mut ResolvedConfig, child: &ResolvedConfig| {
                for (k, v) in &child.merged {
                    heap.merged.entry(k.clone()).or_insert_with(|| v.clone());
                }
            },
            |h: &ResolvedConfig| h.clone(),
        );

        let result: ResolvedConfig = FUSED.run(&resolve, &graph, &root);

        // Global scope sees all keys from all descendants,
        // but its own values win for "color", "font_size", "theme".
        assert_eq!(result.merged.get("color").unwrap(), "blue");
        assert_eq!(result.merged.get("theme").unwrap(), "light");  // parent wins
        assert_eq!(result.merged.get("debug").unwrap(), "false");  // production's value
        assert_eq!(result.merged.get("rate_limit").unwrap(), "1000");

        let display: Vec<String> = result.merged.iter()
            .map(|(k, v)| format!("{k}={v}")).collect();
        assert_snapshot!("config", display.join(", "));
    }
}
}

Output:

color=blue, debug=false, font_size=12, rate_limit=1000, theme=light