use crate::config::FlowControlConfig;
use crate::error::LimiteronError;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;
pub async fn load_config_from_file(path: &str) -> Result<FlowControlConfig, LimiteronError> {
let content = tokio::fs::read_to_string(path)
.await
.map_err(|e| LimiteronError::ConfigError(format!("failed to read config file: {}", e)))?;
let format = detect_format(path);
parse_config(&content, format)
}
pub type GovernorConfigHandle = Arc<RwLock<FlowControlConfig>>;
#[cfg(feature = "config-confers-reload")]
pub async fn watch_and_reload(
path: &str,
config_handle: GovernorConfigHandle,
) -> Result<tokio_util::sync::CancellationToken, LimiteronError> {
let path_buf = std::path::PathBuf::from(path);
let mut watcher = confers::FsWatcher::new(&path_buf, 500)
.await
.map_err(|e| LimiteronError::ConfigError(format!("failed to create watcher: {}", e)))?;
let cancel_token = tokio_util::sync::CancellationToken::new();
let cancel_clone = cancel_token.clone();
let file_path = path.to_string();
tokio::spawn(async move {
loop {
tokio::select! {
_ = cancel_clone.cancelled() => {
log::info!("confers watcher: cancelled, stopping");
break;
}
event = watcher.recv() => {
match event {
Some(_changed_path) => {
log::info!("confers watcher: config file changed, reloading");
match load_config_from_file(&file_path).await {
Ok(new_config) => {
if new_config.rules.is_empty() {
log::warn!(
"confers reload: new config has empty rules, rolling back"
);
continue;
}
let mut guard = config_handle.write().await;
*guard = new_config;
log::info!("confers reload: config updated successfully");
}
Err(e) => {
log::error!(
"confers reload: failed to parse config, rolling back: {}",
e
);
}
}
}
None => {
log::warn!("confers watcher: watcher stopped unexpectedly");
break;
}
}
}
}
}
});
Ok(cancel_token)
}
#[derive(Debug, Clone, Copy)]
enum ConfigFormat {
Toml,
Json,
Yaml,
}
fn detect_format(path: &str) -> ConfigFormat {
match Path::new(path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
{
"json" => ConfigFormat::Json,
"yaml" | "yml" => ConfigFormat::Yaml,
_ => ConfigFormat::Toml, }
}
fn parse_config(content: &str, format: ConfigFormat) -> Result<FlowControlConfig, LimiteronError> {
match format {
ConfigFormat::Toml => toml::from_str(content)
.map_err(|e| LimiteronError::ConfigError(format!("TOML parse error: {}", e))),
ConfigFormat::Json => serde_json::from_str(content)
.map_err(|e| LimiteronError::ConfigError(format!("JSON parse error: {}", e))),
ConfigFormat::Yaml => serde_yaml_ng::from_str(content)
.map_err(|e| LimiteronError::ConfigError(format!("YAML parse error: {}", e))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_format_toml() {
assert!(matches!(detect_format("config.toml"), ConfigFormat::Toml));
}
#[test]
fn test_detect_format_json() {
assert!(matches!(detect_format("config.json"), ConfigFormat::Json));
}
#[test]
fn test_detect_format_yaml() {
assert!(matches!(detect_format("config.yaml"), ConfigFormat::Yaml));
assert!(matches!(detect_format("config.yml"), ConfigFormat::Yaml));
}
#[test]
fn test_detect_format_default() {
assert!(matches!(detect_format("config"), ConfigFormat::Toml));
assert!(matches!(detect_format("config.txt"), ConfigFormat::Toml));
}
#[test]
fn test_parse_config_json_valid() {
let json = r#"{
"version": "0.1.0",
"global": {
"storage": "memory",
"cache": "memory",
"metrics": "prometheus"
},
"rules": []
}"#;
let result = parse_config(json, ConfigFormat::Json);
assert!(result.is_ok());
let config = result.unwrap();
assert_eq!(config.version, "0.1.0");
}
#[test]
fn test_parse_config_json_invalid() {
let result = parse_config("not json", ConfigFormat::Json);
assert!(result.is_err());
}
#[tokio::test]
async fn test_load_config_from_file_not_found() {
let result = load_config_from_file("/nonexistent/path.toml").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_load_config_from_file_json_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test_config.json");
let config_json = r#"{
"version": "1.0.0",
"global": {
"storage": "memory",
"cache": "memory",
"metrics": "prometheus"
},
"rules": []
}"#;
std::fs::write(&path, config_json).unwrap();
let result = load_config_from_file(path.to_str().unwrap()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().version, "1.0.0");
}
}