Skip to main content

forgeconf_core/source/
mod.rs

1use crate::{ConfigError, ConfigNode};
2
3#[cfg(feature = "cli")]
4mod cli;
5mod file;
6
7#[cfg(feature = "cli")]
8pub use cli::CliArguments;
9pub use file::ConfigFile;
10
11/// Trait implemented by configuration sources (files, CLI, etc).
12pub trait ConfigSource: Send + Sync {
13    /// Higher priority sources override lower priority ones.
14    fn priority(&self) -> u8 {
15        0
16    }
17
18    /// Load configuration data from the source.
19    fn load(&self) -> Result<ConfigNode, ConfigError>;
20}
21
22/// Combine two configuration trees, where values from `overlay` take
23/// precedence.
24pub fn merge_nodes(base: ConfigNode, overlay: ConfigNode) -> ConfigNode {
25    match (base, overlay) {
26        (ConfigNode::Table(mut left), ConfigNode::Table(right)) => {
27            for (key, value) in right {
28                match left.remove(&key) {
29                    Some(existing) => {
30                        let merged = merge_nodes(existing, value);
31                        left.insert(key, merged);
32                    },
33                    None => {
34                        left.insert(key, value);
35                    },
36                }
37            }
38            ConfigNode::Table(left)
39        },
40        (_, other) => other,
41    }
42}
43
44/// Builder that merges a set of [`ConfigSource`] instances.
45#[derive(Default)]
46pub struct ConfigBuilder {
47    sources: Vec<Box<dyn ConfigSource>>,
48}
49
50impl ConfigBuilder {
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    pub fn add_source<S>(mut self, source: S) -> Self
56    where
57        S: ConfigSource + 'static,
58    {
59        self.sources
60            .push(Box::new(source));
61        self
62    }
63
64    pub fn load(mut self) -> Result<ConfigNode, ConfigError> {
65        self.sources
66            .sort_by_key(|source| source.priority());
67
68        let mut merged = ConfigNode::empty_table();
69        for source in self.sources {
70            let value = source.load()?;
71            merged = merge_nodes(merged, value);
72        }
73
74        Ok(merged)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use std::collections::BTreeMap;
81
82    use super::*;
83
84    #[derive(Clone)]
85    struct StaticSource {
86        priority: u8,
87        node: ConfigNode,
88    }
89
90    impl StaticSource {
91        fn table(priority: u8, key: &str, value: &str) -> Self {
92            let mut map = BTreeMap::new();
93            map.insert(key.to_string(), ConfigNode::Scalar(value.to_string()));
94            Self { priority, node: ConfigNode::Table(map) }
95        }
96    }
97
98    impl ConfigSource for StaticSource {
99        fn priority(&self) -> u8 {
100            self.priority
101        }
102
103        fn load(&self) -> Result<ConfigNode, ConfigError> {
104            Ok(self
105                .node
106                .clone())
107        }
108    }
109
110    #[test]
111    fn merge_nodes_prefers_overlay() {
112        let mut left = BTreeMap::new();
113        left.insert("port".into(), ConfigNode::Scalar("8080".into()));
114
115        let mut right = BTreeMap::new();
116        right.insert("port".into(), ConfigNode::Scalar("9090".into()));
117        right.insert("host".into(), ConfigNode::Scalar("0.0.0.0".into()));
118
119        let merged = merge_nodes(ConfigNode::Table(left), ConfigNode::Table(right));
120        let table = merged
121            .as_table()
122            .unwrap();
123        assert_eq!(
124            table
125                .get("port")
126                .unwrap()
127                .to_string(),
128            "9090"
129        );
130        assert!(table.contains_key("host"));
131    }
132
133    #[test]
134    fn merge_nodes_replaces_non_tables() {
135        let merged =
136            merge_nodes(ConfigNode::Scalar("base".into()), ConfigNode::Scalar("override".into()));
137        assert_eq!(merged.to_string(), "override");
138    }
139
140    #[test]
141    fn config_builder_honors_source_priority() {
142        let node = ConfigBuilder::new()
143            .add_source(StaticSource::table(5, "service", "base"))
144            .add_source(StaticSource::table(200, "service", "override"))
145            .load()
146            .unwrap();
147
148        let table = node
149            .as_table()
150            .unwrap();
151        assert_eq!(
152            table
153                .get("service")
154                .unwrap()
155                .to_string(),
156            "override"
157        );
158    }
159}