feat(protocol): load_all_configs from dir with duplicate port detection

This commit is contained in:
2026-05-25 11:30:38 +02:00
parent 7e59d7d050
commit e8f5846cec
2 changed files with 88 additions and 1 deletions
+87
View File
@@ -259,6 +259,58 @@ fn find_node<'a>(doc: &'a KdlDocument, name: &str) -> Option<&'a KdlNode> {
doc.nodes().iter().find(|n| n.name().value() == name)
}
pub fn load_all_configs(dir: &Path) -> Result<Vec<ServerConfig>, ConfigError> {
if !dir.exists() {
return Ok(Vec::new());
}
let entries = std::fs::read_dir(dir).map_err(|e| ConfigError::Io {
path: dir.to_path_buf(),
source: e,
})?;
let mut configs = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| ConfigError::Io {
path: dir.to_path_buf(),
source: e,
})?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("kdl") {
continue;
}
let name = path
.file_stem()
.and_then(|s| s.to_str())
.ok_or(ConfigError::InvalidName {
name: path.display().to_string(),
})?
.to_string();
let text = std::fs::read_to_string(&path).map_err(|e| ConfigError::Io {
path: path.clone(),
source: e,
})?;
configs.push(parse_server_config(&name, &text, &path)?);
}
check_duplicate_ports(&configs)?;
Ok(configs)
}
fn check_duplicate_ports(configs: &[ServerConfig]) -> Result<(), ConfigError> {
let mut seen: std::collections::HashMap<u16, String> = std::collections::HashMap::new();
for c in configs {
if let Some(other) = seen.insert(c.port, c.name.clone()) {
return Err(ConfigError::DuplicatePort {
name_a: other,
name_b: c.name.clone(),
port: c.port,
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -357,4 +409,39 @@ stop {
assert!(matches!(err, ConfigError::InvalidName { .. }));
}
use std::fs;
use tempfile::tempdir;
#[test]
fn load_all_finds_and_parses_files() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("a.kdl"), "command \"/bin/a\"\nport 8001").unwrap();
fs::write(dir.path().join("b.kdl"), "command \"/bin/b\"\nport 8002").unwrap();
fs::write(dir.path().join("ignored.txt"), "not a config").unwrap();
let mut configs = load_all_configs(dir.path()).unwrap();
configs.sort_by(|x, y| x.name.cmp(&y.name));
assert_eq!(configs.len(), 2);
assert_eq!(configs[0].name, "a");
assert_eq!(configs[1].port, 8002);
}
#[test]
fn load_all_returns_empty_for_missing_dir() {
let dir = tempdir().unwrap();
let configs = load_all_configs(&dir.path().join("does-not-exist")).unwrap();
assert!(configs.is_empty());
}
#[test]
fn duplicate_ports_detected() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("a.kdl"), "command \"/bin/a\"\nport 8001").unwrap();
fs::write(dir.path().join("b.kdl"), "command \"/bin/b\"\nport 8001").unwrap();
let err = load_all_configs(dir.path()).unwrap_err();
match err {
ConfigError::DuplicatePort { port, .. } => assert_eq!(port, 8001),
other => panic!("unexpected error: {other:?}"),
}
}
}
+1 -1
View File
@@ -7,5 +7,5 @@ pub mod state;
pub use config::{RestartConfig, RestartPolicy, ServerConfig, StopConfig};
pub use error::{ConfigError, RpcErrorCode};
pub use kdl_parse::parse_server_config;
pub use kdl_parse::{load_all_configs, parse_server_config};
pub use state::ServerState;