use crate::yahoo_error::YahooError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::SystemTime;
use std::fs;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ConfigSource {
File(PathBuf),
Environment,
CommandLine,
Remote(String),
Memory,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConfigFormat {
Toml,
Json,
Yaml,
Env,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigProfile {
pub name: String,
pub description: String,
pub inherits_from: Option<String>,
pub rate_limit: f64,
pub circuit_breaker: CircuitBreakerConfig,
pub cache: CacheConfig,
pub retry: RetryConfig,
pub timeouts: TimeoutConfig,
pub observability: ObservabilityConfig,
pub custom: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CircuitBreakerConfig {
pub threshold: u32,
pub window_secs: u64,
pub timeout_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
pub size: usize,
pub ttl_secs: u64,
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryConfig {
pub max_attempts: u32,
pub initial_delay_ms: u64,
pub max_delay_ms: u64,
pub backoff_multiplier: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeoutConfig {
pub request_timeout_secs: u64,
pub connection_timeout_secs: u64,
pub read_timeout_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObservabilityConfig {
pub metrics_enabled: bool,
pub tracing_enabled: bool,
pub health_checks_enabled: bool,
pub log_level: String,
}
#[derive(Debug, Clone)]
pub struct ValidationRule {
pub name: String,
pub description: String,
pub validator: fn(&ConfigProfile) -> Result<(), String>,
}
#[derive(Debug)]
pub struct ConfigManager {
active_config: Arc<RwLock<ConfigProfile>>,
profiles: Arc<RwLock<HashMap<String, ConfigProfile>>>,
sources: Arc<RwLock<HashMap<ConfigSource, SystemTime>>>,
validation_rules: Vec<ValidationRule>,
watchers: Arc<RwLock<HashMap<PathBuf, tokio::sync::watch::Receiver<()>>>>,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
threshold: 5,
window_secs: 300,
timeout_secs: 60,
}
}
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
size: 1000,
ttl_secs: 300,
enabled: true,
}
}
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
initial_delay_ms: 1000,
max_delay_ms: 30000,
backoff_multiplier: 2.0,
}
}
}
impl Default for TimeoutConfig {
fn default() -> Self {
Self {
request_timeout_secs: 30,
connection_timeout_secs: 10,
read_timeout_secs: 30,
}
}
}
impl Default for ObservabilityConfig {
fn default() -> Self {
Self {
metrics_enabled: true,
tracing_enabled: true,
health_checks_enabled: true,
log_level: "info".to_string(),
}
}
}
impl Default for ConfigProfile {
fn default() -> Self {
Self {
name: "default".to_string(),
description: "Default configuration profile".to_string(),
inherits_from: None,
rate_limit: 1.0, circuit_breaker: CircuitBreakerConfig::default(),
cache: CacheConfig::default(),
retry: RetryConfig::default(),
timeouts: TimeoutConfig::default(),
observability: ObservabilityConfig::default(),
custom: HashMap::new(),
}
}
}
impl ConfigProfile {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
..Default::default()
}
}
pub fn development() -> Self {
Self {
name: "development".to_string(),
description: "Development environment configuration".to_string(),
rate_limit: 5.0, cache: CacheConfig {
size: 500,
ttl_secs: 60, enabled: true,
},
retry: RetryConfig {
max_attempts: 2, ..RetryConfig::default()
},
observability: ObservabilityConfig {
metrics_enabled: true,
tracing_enabled: true,
health_checks_enabled: true,
log_level: "debug".to_string(), },
..Default::default()
}
}
pub fn production() -> Self {
Self {
name: "production".to_string(),
description: "Production environment configuration".to_string(),
rate_limit: 0.5, circuit_breaker: CircuitBreakerConfig {
threshold: 3, window_secs: 180,
timeout_secs: 30,
},
cache: CacheConfig {
size: 5000, ttl_secs: 900, enabled: true,
},
retry: RetryConfig {
max_attempts: 5, initial_delay_ms: 2000,
max_delay_ms: 60000,
..RetryConfig::default()
},
observability: ObservabilityConfig {
log_level: "info".to_string(), ..ObservabilityConfig::default()
},
..Default::default()
}
}
pub fn merge_with(&mut self, parent: &ConfigProfile) -> Result<(), YahooError> {
if self.inherits_from.as_ref() == Some(&parent.name) {
if self.rate_limit == ConfigProfile::default().rate_limit {
self.rate_limit = parent.rate_limit;
}
}
Ok(())
}
pub fn validate(&self) -> Result<(), YahooError> {
if self.rate_limit <= 0.0 {
return Err(YahooError::InvalidStatusCode(
"Rate limit must be positive".into()
));
}
if self.circuit_breaker.threshold == 0 {
return Err(YahooError::InvalidStatusCode(
"Circuit breaker threshold must be positive".into()
));
}
if self.cache.size == 0 {
return Err(YahooError::InvalidStatusCode(
"Cache size must be positive".into()
));
}
if self.retry.max_attempts == 0 {
return Err(YahooError::InvalidStatusCode(
"Retry max attempts must be positive".into()
));
}
Ok(())
}
}
impl ConfigManager {
pub fn new() -> Self {
let mut profiles = HashMap::new();
profiles.insert("default".to_string(), ConfigProfile::default());
profiles.insert("development".to_string(), ConfigProfile::development());
profiles.insert("production".to_string(), ConfigProfile::production());
Self {
active_config: Arc::new(RwLock::new(ConfigProfile::default())),
profiles: Arc::new(RwLock::new(profiles)),
sources: Arc::new(RwLock::new(HashMap::new())),
validation_rules: Self::default_validation_rules(),
watchers: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn load_from_file<P: AsRef<Path>>(&self, path: P) -> Result<(), YahooError> {
let path = path.as_ref();
let content = fs::read_to_string(path)
.map_err(|e| YahooError::InvalidStatusCode(format!("Failed to read config file: {}", e)))?;
let format = Self::detect_format(path)?;
let profile = self.parse_config(&content, format)?;
self.add_profile(profile).await?;
let mut sources = self.sources.write().unwrap();
sources.insert(ConfigSource::File(path.to_path_buf()), SystemTime::now());
Ok(())
}
pub async fn load_from_env(&self, prefix: &str) -> Result<(), YahooError> {
let mut profile = ConfigProfile::default();
profile.name = "environment".to_string();
profile.description = "Configuration loaded from environment variables".to_string();
for (key, value) in std::env::vars() {
if let Some(config_key) = key.strip_prefix(prefix) {
match config_key {
"RATE_LIMIT" => {
profile.rate_limit = value.parse().map_err(|_| {
YahooError::InvalidStatusCode("Invalid RATE_LIMIT value".into())
})?;
}
"CACHE_SIZE" => {
profile.cache.size = value.parse().map_err(|_| {
YahooError::InvalidStatusCode("Invalid CACHE_SIZE value".into())
})?;
}
"LOG_LEVEL" => {
profile.observability.log_level = value;
}
_ => {
profile.custom.insert(
config_key.to_lowercase(),
serde_json::Value::String(value)
);
}
}
}
}
self.add_profile(profile).await?;
let mut sources = self.sources.write().unwrap();
sources.insert(ConfigSource::Environment, SystemTime::now());
Ok(())
}
pub async fn add_profile(&self, profile: ConfigProfile) -> Result<(), YahooError> {
profile.validate()?;
for rule in &self.validation_rules {
(rule.validator)(&profile).map_err(|e| {
YahooError::InvalidStatusCode(format!("Validation rule '{}' failed: {}", rule.name, e))
})?;
}
let mut profiles = self.profiles.write().unwrap();
profiles.insert(profile.name.clone(), profile);
Ok(())
}
pub async fn set_active_profile(&self, name: &str) -> Result<(), YahooError> {
let profiles = self.profiles.read().unwrap();
let profile = profiles.get(name).ok_or_else(|| {
YahooError::InvalidStatusCode(format!("Profile '{}' not found", name))
})?.clone();
drop(profiles);
let mut active = self.active_config.write().unwrap();
*active = profile;
Ok(())
}
pub fn get_active_config(&self) -> ConfigProfile {
self.active_config.read().unwrap().clone()
}
pub fn list_profiles(&self) -> Vec<String> {
self.profiles.read().unwrap().keys().cloned().collect()
}
pub async fn enable_hot_reload<P: AsRef<Path>>(&self, path: P) -> Result<(), YahooError> {
let _path = path.as_ref().to_path_buf();
println!("Hot reload enabled for configuration files");
Ok(())
}
fn detect_format(path: &Path) -> Result<ConfigFormat, YahooError> {
match path.extension().and_then(|ext| ext.to_str()) {
Some("toml") => Ok(ConfigFormat::Toml),
Some("json") => Ok(ConfigFormat::Json),
Some("yaml" | "yml") => Ok(ConfigFormat::Yaml),
_ => Err(YahooError::InvalidStatusCode(
"Unsupported configuration file format".into()
))
}
}
fn parse_config(&self, content: &str, format: ConfigFormat) -> Result<ConfigProfile, YahooError> {
match format {
ConfigFormat::Toml => {
toml::from_str(content).map_err(|e| {
YahooError::InvalidStatusCode(format!("Failed to parse TOML config: {}", e))
})
}
ConfigFormat::Json => {
serde_json::from_str(content).map_err(|e| {
YahooError::InvalidStatusCode(format!("Failed to parse JSON config: {}", e))
})
}
ConfigFormat::Yaml => {
Err(YahooError::InvalidStatusCode("YAML parsing not implemented".into()))
}
ConfigFormat::Env => {
Err(YahooError::InvalidStatusCode("ENV format not supported for file parsing".into()))
}
}
}
fn default_validation_rules() -> Vec<ValidationRule> {
vec![
ValidationRule {
name: "positive_rate_limit".to_string(),
description: "Rate limit must be positive".to_string(),
validator: |config| {
if config.rate_limit <= 0.0 {
Err("Rate limit must be positive".to_string())
} else {
Ok(())
}
},
},
ValidationRule {
name: "reasonable_cache_size".to_string(),
description: "Cache size should be reasonable (1-100000)".to_string(),
validator: |config| {
if config.cache.size == 0 || config.cache.size > 100_000 {
Err("Cache size should be between 1 and 100,000".to_string())
} else {
Ok(())
}
},
},
]
}
}
impl Default for ConfigManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct ConfigBuilder {
profile: ConfigProfile,
}
impl ConfigBuilder {
pub fn new(name: &str) -> Self {
Self {
profile: ConfigProfile::new(name),
}
}
pub fn rate_limit(mut self, rate: f64) -> Self {
self.profile.rate_limit = rate;
self
}
pub fn cache(mut self, size: usize, ttl_secs: u64) -> Self {
self.profile.cache = CacheConfig {
size,
ttl_secs,
enabled: true,
};
self
}
pub fn circuit_breaker(mut self, threshold: u32, window_secs: u64, timeout_secs: u64) -> Self {
self.profile.circuit_breaker = CircuitBreakerConfig {
threshold,
window_secs,
timeout_secs,
};
self
}
pub fn custom<T: serde::Serialize>(mut self, key: &str, value: T) -> Self {
if let Ok(json_value) = serde_json::to_value(value) {
self.profile.custom.insert(key.to_string(), json_value);
}
self
}
pub fn inherits_from(mut self, parent: &str) -> Self {
self.profile.inherits_from = Some(parent.to_string());
self
}
pub fn build(self) -> Result<ConfigProfile, YahooError> {
self.profile.validate()?;
Ok(self.profile)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_profile_validation() {
let mut profile = ConfigProfile::default();
assert!(profile.validate().is_ok());
profile.rate_limit = -1.0;
assert!(profile.validate().is_err());
}
#[test]
fn test_config_builder() {
let profile = ConfigBuilder::new("test")
.rate_limit(2.0)
.cache(1000, 300)
.circuit_breaker(5, 300, 60)
.custom("test_setting", "test_value")
.build()
.unwrap();
assert_eq!(profile.name, "test");
assert_eq!(profile.rate_limit, 2.0);
assert_eq!(profile.cache.size, 1000);
assert_eq!(profile.circuit_breaker.threshold, 5);
}
#[tokio::test]
async fn test_config_manager() {
let manager = ConfigManager::new();
let profiles = manager.list_profiles();
assert!(profiles.contains(&"default".to_string()));
assert!(profiles.contains(&"development".to_string()));
assert!(profiles.contains(&"production".to_string()));
manager.set_active_profile("production").await.unwrap();
let active = manager.get_active_config();
assert_eq!(active.name, "production");
}
}