sentinel_config/multi_file/
mod.rs1mod builder;
42mod directory;
43mod loader;
44mod parsers;
45
46pub use directory::ConfigDirectory;
47pub use loader::MultiFileLoader;
48
49use anyhow::{anyhow, Result};
50use std::path::Path;
51
52use crate::Config;
53
54impl Config {
56 pub fn from_directory(path: impl AsRef<Path>) -> Result<Self> {
60 let mut loader = MultiFileLoader::new(path);
61 loader.load()
62 }
63
64 pub fn from_directory_with_env(path: impl AsRef<Path>, environment: &str) -> Result<Self> {
69 let dir = ConfigDirectory::new(path);
70 dir.load(Some(environment))
71 }
72
73 pub fn merge(&mut self, other: Config) -> Result<()> {
78 for listener in other.listeners {
80 if self.listeners.iter().any(|l| l.id == listener.id) {
81 return Err(anyhow!("Duplicate listener ID: {}", listener.id));
82 }
83 self.listeners.push(listener);
84 }
85
86 for route in other.routes {
88 if self.routes.iter().any(|r| r.id == route.id) {
89 return Err(anyhow!("Duplicate route ID: {}", route.id));
90 }
91 self.routes.push(route);
92 }
93
94 self.upstreams.extend(other.upstreams);
96
97 for agent in other.agents {
99 if self.agents.iter().any(|a| a.id == agent.id) {
100 return Err(anyhow!("Duplicate agent ID: {}", agent.id));
101 }
102 self.agents.push(agent);
103 }
104
105 Ok(())
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use std::fs;
113 use tempfile::TempDir;
114
115 #[test]
116 fn test_multi_file_loading() {
117 let temp_dir = TempDir::new().unwrap();
118 let config_dir = temp_dir.path();
119
120 fs::write(
122 config_dir.join("main.kdl"),
123 r#"
124 server {
125 worker-threads 2
126 max-connections 1000
127 }
128 limits {
129 max-header-size 8192
130 }
131 "#,
132 )
133 .unwrap();
134
135 fs::create_dir(config_dir.join("routes")).unwrap();
136 fs::write(
137 config_dir.join("routes/api.kdl"),
138 r#"
139 route "api" {
140 path "/api/*"
141 upstream "backend"
142 }
143 "#,
144 )
145 .unwrap();
146
147 let mut loader = MultiFileLoader::new(config_dir);
149 let config = loader.load();
150
151 assert!(config.is_ok(), "Config load failed: {:?}", config.err());
152 }
153
154 #[test]
155 fn test_duplicate_detection() {
156 let temp_dir = TempDir::new().unwrap();
157 let config_dir = temp_dir.path();
158
159 fs::write(
161 config_dir.join("routes1.kdl"),
162 r#"
163 route "api" {
164 path "/api/*"
165 }
166 "#,
167 )
168 .unwrap();
169
170 fs::write(
171 config_dir.join("routes2.kdl"),
172 r#"
173 route "api" {
174 path "/api/v2/*"
175 }
176 "#,
177 )
178 .unwrap();
179
180 let mut loader = MultiFileLoader::new(config_dir);
181 let result = loader.load();
182
183 assert!(result.is_err());
185 }
186
187 #[test]
188 fn test_environment_overrides() {
189 let temp_dir = TempDir::new().unwrap();
190 let config_dir = temp_dir.path();
191
192 fs::write(
194 config_dir.join("sentinel.kdl"),
195 r#"
196 server {
197 worker-threads 2
198 max-connections 1000
199 }
200 limits {
201 max-connections 1000
202 }
203 "#,
204 )
205 .unwrap();
206
207 fs::create_dir(config_dir.join("environments")).unwrap();
209 fs::write(
210 config_dir.join("environments/production.kdl"),
211 r#"
212 limits {
213 max-connections 10000
214 }
215 "#,
216 )
217 .unwrap();
218
219 let config_dir = ConfigDirectory::new(config_dir);
221 let config = config_dir.load(Some("production"));
222
223 assert!(config.is_ok(), "Config load failed: {:?}", config.err());
224 }
226
227 #[test]
228 fn test_include_processing() {
229 let temp_dir = TempDir::new().unwrap();
230 let config_dir = temp_dir.path();
231
232 fs::write(
234 config_dir.join("routes.kdl"),
235 r#"
236 route "api" {
237 path "/api/*"
238 upstream "backend"
239 }
240 "#,
241 )
242 .unwrap();
243
244 fs::write(
246 config_dir.join("main.kdl"),
247 r#"
248 include "routes.kdl"
249
250 server {
251 worker-threads 2
252 max-connections 1000
253 }
254
255 upstream "backend" {
256 address "127.0.0.1:8080"
257 }
258 "#,
259 )
260 .unwrap();
261
262 let mut loader = MultiFileLoader::new(config_dir);
264 let config = loader.load();
265
266 assert!(config.is_ok(), "Config load failed: {:?}", config.err());
267 let config = config.unwrap();
268
269 assert_eq!(config.routes.len(), 1);
271 assert_eq!(config.routes[0].id, "api");
272 }
273
274 #[test]
275 fn test_circular_include_prevention() {
276 let temp_dir = TempDir::new().unwrap();
277 let config_dir = temp_dir.path();
278
279 fs::write(
281 config_dir.join("a.kdl"),
282 r#"
283 include "b.kdl"
284 server {
285 worker-threads 2
286 }
287 "#,
288 )
289 .unwrap();
290
291 fs::write(
292 config_dir.join("b.kdl"),
293 r#"
294 include "a.kdl"
295 route "test" {
296 path "/*"
297 }
298 "#,
299 )
300 .unwrap();
301
302 let mut loader = MultiFileLoader::new(config_dir);
304 let config = loader.load();
305
306 assert!(config.is_ok(), "Config load failed: {:?}", config.err());
308 }
309}