forgeconf_core/source/
mod.rs1use 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
11pub trait ConfigSource: Send + Sync {
13 fn priority(&self) -> u8 {
15 0
16 }
17
18 fn load(&self) -> Result<ConfigNode, ConfigError>;
20}
21
22pub 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#[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}