mod builder;
mod directory;
mod loader;
mod parsers;
pub use directory::ConfigDirectory;
pub use loader::MultiFileLoader;
use anyhow::{anyhow, Result};
use std::path::Path;
use crate::Config;
impl Config {
pub fn from_directory(path: impl AsRef<Path>) -> Result<Self> {
let mut loader = MultiFileLoader::new(path);
loader.load()
}
pub fn from_directory_with_env(path: impl AsRef<Path>, environment: &str) -> Result<Self> {
let dir = ConfigDirectory::new(path);
dir.load(Some(environment))
}
pub fn merge(&mut self, other: Config) -> Result<()> {
for listener in other.listeners {
if self.listeners.iter().any(|l| l.id == listener.id) {
return Err(anyhow!("Duplicate listener ID: {}", listener.id));
}
self.listeners.push(listener);
}
for route in other.routes {
if self.routes.iter().any(|r| r.id == route.id) {
return Err(anyhow!("Duplicate route ID: {}", route.id));
}
self.routes.push(route);
}
self.upstreams.extend(other.upstreams);
for agent in other.agents {
if self.agents.iter().any(|a| a.id == agent.id) {
return Err(anyhow!("Duplicate agent ID: {}", agent.id));
}
self.agents.push(agent);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_multi_file_loading() {
let temp_dir = TempDir::new().unwrap();
let config_dir = temp_dir.path();
fs::write(
config_dir.join("main.kdl"),
r#"
server {
worker-threads 2
max-connections 1000
}
limits {
max-header-size 8192
}
"#,
)
.unwrap();
fs::create_dir(config_dir.join("routes")).unwrap();
fs::write(
config_dir.join("routes/api.kdl"),
r#"
route "api" {
path "/api/*"
upstream "backend"
}
"#,
)
.unwrap();
let mut loader = MultiFileLoader::new(config_dir);
let config = loader.load();
assert!(config.is_ok(), "Config load failed: {:?}", config.err());
}
#[test]
fn test_duplicate_detection() {
let temp_dir = TempDir::new().unwrap();
let config_dir = temp_dir.path();
fs::write(
config_dir.join("routes1.kdl"),
r#"
route "api" {
path "/api/*"
}
"#,
)
.unwrap();
fs::write(
config_dir.join("routes2.kdl"),
r#"
route "api" {
path "/api/v2/*"
}
"#,
)
.unwrap();
let mut loader = MultiFileLoader::new(config_dir);
let result = loader.load();
assert!(result.is_err());
}
#[test]
fn test_environment_overrides() {
let temp_dir = TempDir::new().unwrap();
let config_dir = temp_dir.path();
fs::write(
config_dir.join("zentinel.kdl"),
r#"
server {
worker-threads 2
max-connections 1000
}
limits {
max-connections 1000
}
"#,
)
.unwrap();
fs::create_dir(config_dir.join("environments")).unwrap();
fs::write(
config_dir.join("environments/production.kdl"),
r#"
limits {
max-connections 10000
}
"#,
)
.unwrap();
let config_dir = ConfigDirectory::new(config_dir);
let config = config_dir.load(Some("production"));
assert!(config.is_ok(), "Config load failed: {:?}", config.err());
}
#[test]
fn test_include_processing() {
let temp_dir = TempDir::new().unwrap();
let config_dir = temp_dir.path();
fs::write(
config_dir.join("routes.kdl"),
r#"
route "api" {
path "/api/*"
upstream "backend"
}
"#,
)
.unwrap();
fs::write(
config_dir.join("main.kdl"),
r#"
include "routes.kdl"
server {
worker-threads 2
max-connections 1000
}
upstream "backend" {
address "127.0.0.1:8080"
}
"#,
)
.unwrap();
let mut loader = MultiFileLoader::new(config_dir);
let config = loader.load();
assert!(config.is_ok(), "Config load failed: {:?}", config.err());
let config = config.unwrap();
assert_eq!(config.routes.len(), 1);
assert_eq!(config.routes[0].id, "api");
}
#[test]
fn test_circular_include_prevention() {
let temp_dir = TempDir::new().unwrap();
let config_dir = temp_dir.path();
fs::write(
config_dir.join("a.kdl"),
r#"
include "b.kdl"
server {
worker-threads 2
}
"#,
)
.unwrap();
fs::write(
config_dir.join("b.kdl"),
r#"
include "a.kdl"
route "test" {
path "/*"
}
"#,
)
.unwrap();
let mut loader = MultiFileLoader::new(config_dir);
let config = loader.load();
assert!(config.is_ok(), "Config load failed: {:?}", config.err());
}
#[test]
fn test_namespace_loading() {
let temp_dir = TempDir::new().unwrap();
let config_dir = temp_dir.path();
fs::write(
config_dir.join("main.kdl"),
r#"
server {
worker-threads 2
max-connections 1000
}
namespace "api" {
upstream "backend" {
address "127.0.0.1:8080"
}
route "api-route" {
path "/api/*"
upstream "backend"
}
service "payments" {
route "checkout" {
path "/checkout/*"
}
}
exports {
upstreams "backend"
}
}
"#,
)
.unwrap();
let mut loader = MultiFileLoader::new(config_dir);
let config = loader.load();
assert!(config.is_ok(), "Config load failed: {:?}", config.err());
let config = config.unwrap();
assert_eq!(config.namespaces.len(), 1);
assert_eq!(config.namespaces[0].id, "api");
assert_eq!(config.namespaces[0].upstreams.len(), 1);
assert_eq!(config.namespaces[0].routes.len(), 1);
assert_eq!(config.namespaces[0].services.len(), 1);
assert_eq!(config.namespaces[0].services[0].id, "payments");
assert_eq!(config.namespaces[0].exports.upstreams.len(), 1);
}
#[test]
fn test_duplicate_namespace_detection() {
let temp_dir = TempDir::new().unwrap();
let config_dir = temp_dir.path();
fs::write(
config_dir.join("ns1.kdl"),
r#"
namespace "api" {
route "route1" {
path "/v1/*"
}
}
"#,
)
.unwrap();
fs::write(
config_dir.join("ns2.kdl"),
r#"
namespace "api" {
route "route2" {
path "/v2/*"
}
}
"#,
)
.unwrap();
let mut loader = MultiFileLoader::new(config_dir);
let result = loader.load();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Duplicate namespace"));
}
}