//! Core configuration management functionality. use crate::{ ConfigError, ConfigFormat, ConfigLoader, ConfigResult, ConfigValidator, ConfigWatcher, Environment, FeatureFlagManager, LoaderConfig, SecretManager, WatchEvent, }; use dashmap::DashMap; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tokio::sync::{RwLock, broadcast}; use tracing::{debug, error, info, warn}; /// Configuration value that can hold different types. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum ConfigValue { String(String), Integer(i64), Float(f64), Boolean(bool), Array(Vec), Object(HashMap), Null, } impl ConfigValue { /// Convert to a specific type. pub fn try_into(&self) -> ConfigResult where T: DeserializeOwned, { let json_value = serde_json::to_value(self)?; serde_json::from_value(json_value) .map_err(|e| ConfigError::SerializationError { source: e }) } /// Get the type name of this value. pub fn type_name(&self) -> &'static str { match self { ConfigValue::String(_) => "String", ConfigValue::Integer(_) => "Integer", ConfigValue::Float(_) => "Float", ConfigValue::Boolean(_) => "Boolean", ConfigValue::Array(_) => "Array", ConfigValue::Object(_) => "Object", ConfigValue::Null => "Null", } } /// Check if this value is null. pub fn is_null(&self) -> bool { matches!(self, ConfigValue::Null) } /// Get as string, converting if necessary. pub fn as_string(&self) -> ConfigResult { match self { ConfigValue::String(s) => Ok(s.clone()), ConfigValue::Integer(i) => Ok(i.to_string()), ConfigValue::Float(f) => Ok(f.to_string()), ConfigValue::Boolean(b) => Ok(b.to_string()), _ => Err(ConfigError::type_mismatch("", "String", self.type_name())), } } /// Get as integer, converting if necessary. pub fn as_integer(&self) -> ConfigResult { match self { ConfigValue::Integer(i) => Ok(*i), ConfigValue::String(s) => s .parse() .map_err(|_| ConfigError::type_mismatch("", "Integer", "String")), _ => Err(ConfigError::type_mismatch("", "Integer", self.type_name())), } } /// Get as boolean, converting if necessary. pub fn as_boolean(&self) -> ConfigResult { match self { ConfigValue::Boolean(b) => Ok(*b), ConfigValue::String(s) => match s.to_lowercase().as_str() { "true" | "yes" | "1" | "on" => Ok(true), "false" | "no" | "0" | "off" => Ok(false), _ => Err(ConfigError::type_mismatch("", "Boolean", "String")), }, ConfigValue::Integer(i) => Ok(*i != 0), _ => Err(ConfigError::type_mismatch("", "Boolean", self.type_name())), } } } /// Configuration source definition. #[derive(Debug, Clone)] pub enum ConfigSource { /// Load from environment variables Environment, /// Load from a file File { path: PathBuf, format: ConfigFormat, required: bool, }, /// Load from remote URL Remote { url: Option, headers: HashMap, timeout: Duration, }, /// Load from static values Static { values: HashMap, }, } /// Runtime configuration with change notifications. #[derive(Debug, Clone)] pub struct RuntimeConfig { /// Configuration values values: Arc>, /// Change notification broadcaster change_notifier: broadcast::Sender, /// Configuration metadata metadata: Arc>, } /// Configuration change event. #[derive(Debug, Clone)] pub struct ConfigChangeEvent { /// The key that changed pub key: String, /// Old value (if any) pub old_value: Option, /// New value pub new_value: ConfigValue, /// Source of the change pub source: String, /// Timestamp of the change pub timestamp: chrono::DateTime, } /// Configuration metadata. #[derive(Debug, Clone)] pub struct ConfigMetadata { /// When the configuration was last loaded pub last_loaded: chrono::DateTime, /// Which sources were loaded successfully pub loaded_sources: Vec, /// Load errors (non-fatal) pub load_errors: Vec, /// Total number of configuration keys pub key_count: usize, } impl RuntimeConfig { /// Create a new runtime configuration. pub fn new() -> Self { let (change_notifier, _) = broadcast::channel(1000); Self { values: Arc::new(DashMap::new()), change_notifier, metadata: Arc::new(RwLock::new(ConfigMetadata { last_loaded: chrono::Utc::now(), loaded_sources: vec![], load_errors: vec![], key_count: 0, })), } } /// Get a configuration value by key. pub async fn get(&self, key: &str) -> ConfigResult { self.values .get(key) .map(|entry| entry.value().clone()) .ok_or_else(|| ConfigError::key_not_found(key)) } /// Get a typed configuration value. pub async fn get_typed(&self, key: &str) -> ConfigResult where T: DeserializeOwned, { let value = self.get(key).await?; ConfigValue::try_into::(&value).map_err(|e| match e { ConfigError::SerializationError { .. } => { ConfigError::type_mismatch(key, std::any::type_name::(), value.type_name()) } other => other, }) } /// Set a configuration value. pub async fn set(&self, key: &str, value: ConfigValue) -> ConfigResult<()> { let old_value = self.values.get(key).map(|entry| entry.value().clone()); self.values.insert(key.to_string(), value.clone()); // Update metadata { let mut metadata = self.metadata.write().await; metadata.key_count = self.values.len(); } // Notify about the change let change_event = ConfigChangeEvent { key: key.to_string(), old_value, new_value: value, source: "manual".to_string(), timestamp: chrono::Utc::now(), }; // Send notification (ignore errors if no listeners) let _ = self.change_notifier.send(change_event); Ok(()) } /// Remove a configuration key. pub async fn remove(&self, key: &str) -> ConfigResult> { let old_value = self.values.remove(key).map(|(_, v)| v); if let Some(ref value) = old_value { // Update metadata { let mut metadata = self.metadata.write().await; metadata.key_count = self.values.len(); } // Notify about the change let change_event = ConfigChangeEvent { key: key.to_string(), old_value: Some(value.clone()), new_value: ConfigValue::Null, source: "manual".to_string(), timestamp: chrono::Utc::now(), }; let _ = self.change_notifier.send(change_event); } Ok(old_value) } /// Get all configuration keys. pub async fn keys(&self) -> Vec { self.values .iter() .map(|entry| entry.key().clone()) .collect() } /// Check if a key exists. pub async fn contains_key(&self, key: &str) -> bool { self.values.contains_key(key) } /// Get configuration metadata. pub async fn metadata(&self) -> ConfigMetadata { self.metadata.read().await.clone() } /// Subscribe to configuration changes. pub fn subscribe_to_changes(&self) -> broadcast::Receiver { self.change_notifier.subscribe() } /// Merge values from another source. pub async fn merge( &self, values: HashMap, source: &str, ) -> ConfigResult<()> { let mut changes = Vec::new(); for (key, value) in values { let old_value = self.values.get(&key).map(|entry| entry.value().clone()); self.values.insert(key.clone(), value.clone()); changes.push(ConfigChangeEvent { key, old_value, new_value: value, source: source.to_string(), timestamp: chrono::Utc::now(), }); } // Update metadata { let mut metadata = self.metadata.write().await; metadata.key_count = self.values.len(); metadata.last_loaded = chrono::Utc::now(); if !metadata.loaded_sources.contains(&source.to_string()) { metadata.loaded_sources.push(source.to_string()); } } // Send all change notifications for change in changes { let _ = self.change_notifier.send(change); } Ok(()) } /// Clear all configuration values. pub async fn clear(&self) -> ConfigResult<()> { self.values.clear(); // Update metadata { let mut metadata = self.metadata.write().await; metadata.key_count = 0; } Ok(()) } } impl Default for RuntimeConfig { fn default() -> Self { Self::new() } } /// Configuration manager builder. pub struct ConfigManagerBuilder { sources: Vec, hot_reload: bool, validation: bool, secret_management: bool, feature_flags: bool, environment: Option, loader_config: LoaderConfig, } impl ConfigManagerBuilder { /// Create a new builder. pub fn new() -> Self { Self { sources: Vec::new(), hot_reload: false, validation: false, secret_management: false, feature_flags: false, environment: None, loader_config: LoaderConfig::default(), } } /// Add a configuration source. pub fn add_source(mut self, source: ConfigSource) -> Self { self.sources.push(source); self } /// Enable hot reloading. pub fn enable_hot_reload(mut self, enabled: bool) -> Self { self.hot_reload = enabled; self } /// Enable configuration validation. pub fn enable_validation(mut self, enabled: bool) -> Self { self.validation = enabled; self } /// Enable secret management. pub fn enable_secret_management(mut self, enabled: bool) -> Self { self.secret_management = enabled; self } /// Enable feature flags. pub fn enable_feature_flags(mut self, enabled: bool) -> Self { self.feature_flags = enabled; self } /// Set the environment. pub fn environment(mut self, env: Environment) -> Self { self.environment = Some(env); self } /// Set loader configuration. pub fn loader_config(mut self, config: LoaderConfig) -> Self { self.loader_config = config; self } /// Build the configuration manager. pub async fn build(self) -> ConfigResult { let runtime_config = Arc::new(RuntimeConfig::new()); let loader = ConfigLoader::new(self.loader_config); let secret_manager = if self.secret_management { Some(Arc::new(SecretManager::new().await?)) } else { None }; let feature_flag_manager = if self.feature_flags { Some(Arc::new(FeatureFlagManager::new())) } else { None }; let validator = if self.validation { Some(Arc::new(ConfigValidator::new())) } else { None }; let watcher = if self.hot_reload { Some(Arc::new(ConfigWatcher::new(runtime_config.clone()).await?)) } else { None }; let manager = ConfigManager { runtime_config: runtime_config.clone(), sources: self.sources, loader, secret_manager, feature_flag_manager, validator, watcher, environment: self.environment.unwrap_or_default(), }; // Load initial configuration manager.load_all_sources().await?; // Start watching for changes if enabled if let Some(ref watcher) = manager.watcher { manager.start_watching().await?; } Ok(manager) } } impl Default for ConfigManagerBuilder { fn default() -> Self { Self::new() } } /// Main configuration manager. pub struct ConfigManager { /// Runtime configuration runtime_config: Arc, /// Configuration sources sources: Vec, /// Configuration loader loader: ConfigLoader, /// Secret manager secret_manager: Option>, /// Feature flag manager feature_flag_manager: Option>, /// Configuration validator validator: Option>, /// File watcher watcher: Option>, /// Current environment environment: Environment, } impl ConfigManager { /// Create a new builder. pub fn builder() -> ConfigManagerBuilder { ConfigManagerBuilder::new() } /// Get a configuration value. pub async fn get(&self, key: &str) -> ConfigResult where T: DeserializeOwned, { self.runtime_config.get_typed(key).await } /// Set a configuration value. pub async fn set(&self, key: &str, value: ConfigValue) -> ConfigResult<()> { // Validate the value if validation is enabled if let Some(ref validator) = self.validator { validator.validate_value(key, &value).await?; } self.runtime_config.set(key, value).await } /// Remove a configuration key. pub async fn remove(&self, key: &str) -> ConfigResult> { self.runtime_config.remove(key).await } /// Check if a key exists. pub async fn contains_key(&self, key: &str) -> bool { self.runtime_config.contains_key(key).await } /// Get all configuration keys. pub async fn keys(&self) -> Vec { self.runtime_config.keys().await } /// Subscribe to configuration changes. pub fn subscribe_to_changes(&self) -> broadcast::Receiver { self.runtime_config.subscribe_to_changes() } /// Get configuration metadata. pub async fn metadata(&self) -> ConfigMetadata { self.runtime_config.metadata().await } /// Reload configuration from all sources. pub async fn reload(&self) -> ConfigResult<()> { info!("Reloading configuration from all sources"); self.load_all_sources().await } /// Get the secret manager. pub fn secret_manager(&self) -> Option<&Arc> { self.secret_manager.as_ref() } /// Get the feature flag manager. pub fn feature_flag_manager(&self) -> Option<&Arc> { self.feature_flag_manager.as_ref() } /// Check if hot reload is enabled. pub fn is_hot_reload_enabled(&self) -> bool { self.watcher.is_some() } /// Get the current environment. pub fn environment(&self) -> &Environment { &self.environment } /// Load configuration from all sources. async fn load_all_sources(&self) -> ConfigResult<()> { debug!("Loading configuration from {} sources", self.sources.len()); for source in &self.sources { if let Err(e) = self.load_source(source).await { match source { ConfigSource::File { required: true, .. } => { error!("Failed to load required configuration source: {}", e); return Err(e); } _ => { warn!("Failed to load optional configuration source: {}", e); // Continue with other sources } } } } info!("Configuration loaded successfully"); Ok(()) } /// Load configuration from a single source. async fn load_source(&self, source: &ConfigSource) -> ConfigResult<()> { let values = match source { ConfigSource::Environment => { debug!("Loading configuration from environment variables"); self.loader.load_environment().await? } ConfigSource::File { path, format, .. } => { debug!("Loading configuration from file: {}", path.display()); self.loader.load_file(path, *format).await? } ConfigSource::Remote { url: Some(url), headers, timeout, } => { debug!("Loading configuration from remote URL: {}", url); self.loader .load_remote(url, headers.clone(), *timeout) .await? } ConfigSource::Remote { url: None, .. } => { debug!("Skipping remote configuration (no URL provided)"); return Ok(()); } ConfigSource::Static { values } => { debug!("Loading static configuration values"); values.clone() } }; let source_name = match source { ConfigSource::Environment => "environment".to_string(), ConfigSource::File { path, .. } => format!("file:{}", path.display()), ConfigSource::Remote { url: Some(url), .. } => format!("remote:{}", url), ConfigSource::Remote { .. } => "remote:none".to_string(), ConfigSource::Static { .. } => "static".to_string(), }; self.runtime_config.merge(values, &source_name).await?; Ok(()) } /// Start watching for configuration changes. async fn start_watching(&self) -> ConfigResult<()> { if let Some(ref watcher) = self.watcher { // Watch file sources for source in &self.sources { if let ConfigSource::File { path, .. } = source { watcher.watch_file(path).await?; } } // Set up change handler let manager = self.clone(); let mut receiver = watcher.subscribe(); tokio::spawn(async move { while let Ok(event) = receiver.recv().await { if let Err(e) = manager.handle_watch_event(event).await { error!("Failed to handle configuration change: {}", e); } } }); } Ok(()) } /// Handle a file watch event. async fn handle_watch_event(&self, event: WatchEvent) -> ConfigResult<()> { match event { WatchEvent::Modified { path } => { info!("Configuration file modified: {}", path.display()); // Find the corresponding source and reload it for source in &self.sources { if let ConfigSource::File { path: source_path, .. } = source { if source_path == &path { self.load_source(source).await?; break; } } } } WatchEvent::Deleted { path } => { warn!("Configuration file deleted: {}", path.display()); // Handle file deletion - might want to clear related config or use defaults } WatchEvent::Error { error } => { error!("Configuration watcher error: {}", error); } } Ok(()) } } impl Clone for ConfigManager { fn clone(&self) -> Self { Self { runtime_config: self.runtime_config.clone(), sources: self.sources.clone(), loader: self.loader.clone(), secret_manager: self.secret_manager.clone(), feature_flag_manager: self.feature_flag_manager.clone(), validator: self.validator.clone(), watcher: self.watcher.clone(), environment: self.environment.clone(), } } } #[cfg(test)] mod tests { use super::*; use std::io::Write; use tempfile::NamedTempFile; #[tokio::test] async fn test_runtime_config_basic_operations() { let config = RuntimeConfig::new(); // Test set and get config .set("test.string", ConfigValue::String("hello".to_string())) .await .unwrap(); config .set("test.number", ConfigValue::Integer(42)) .await .unwrap(); config .set("test.boolean", ConfigValue::Boolean(true)) .await .unwrap(); let string_value = config.get_typed::("test.string").await.unwrap(); assert_eq!(string_value, "hello"); let number_value = config.get_typed::("test.number").await.unwrap(); assert_eq!(number_value, 42); let bool_value = config.get_typed::("test.boolean").await.unwrap(); assert!(bool_value); // Test key existence assert!(config.contains_key("test.string").await); assert!(!config.contains_key("nonexistent").await); // Test keys let keys = config.keys().await; assert!(keys.contains(&"test.string".to_string())); assert!(keys.contains(&"test.number".to_string())); assert!(keys.contains(&"test.boolean".to_string())); } #[tokio::test] async fn test_config_value_conversions() { let string_val = ConfigValue::String("42".to_string()); assert_eq!(string_val.as_integer().unwrap(), 42); let bool_string = ConfigValue::String("true".to_string()); assert!(bool_string.as_boolean().unwrap()); let int_val = ConfigValue::Integer(1); assert!(int_val.as_boolean().unwrap()); let zero_val = ConfigValue::Integer(0); assert!(!zero_val.as_boolean().unwrap()); } #[tokio::test] async fn test_change_notifications() { let config = RuntimeConfig::new(); let mut receiver = config.subscribe_to_changes(); // Set a value in a separate task let config_clone = config.clone(); tokio::spawn(async move { tokio::time::sleep(Duration::from_millis(10)).await; config_clone .set("test.key", ConfigValue::String("value".to_string())) .await .unwrap(); }); // Receive the change notification let change_event = tokio::time::timeout(Duration::from_millis(100), receiver.recv()) .await .unwrap() .unwrap(); assert_eq!(change_event.key, "test.key"); assert_eq!( change_event.new_value, ConfigValue::String("value".to_string()) ); assert!(change_event.old_value.is_none()); } #[tokio::test] async fn test_config_manager_builder() { let manager = ConfigManager::builder() .add_source(ConfigSource::Environment) .enable_hot_reload(false) .enable_validation(true) .build() .await .unwrap(); assert!(!manager.is_hot_reload_enabled()); assert_eq!(manager.sources.len(), 1); } #[tokio::test] async fn test_config_from_file() { let mut temp_file = NamedTempFile::new().unwrap(); writeln!( temp_file, r#" database_url = "postgres://localhost/test" port = 8080 debug = true "# ) .unwrap(); let manager = ConfigManager::builder() .add_source(ConfigSource::File { path: temp_file.path().to_path_buf(), format: ConfigFormat::Toml, required: true, }) .build() .await .unwrap(); let database_url = manager.get::("database_url").await.unwrap(); assert_eq!(database_url, "postgres://localhost/test"); let port = manager.get::("port").await.unwrap(); assert_eq!(port, 8080); let debug = manager.get::("debug").await.unwrap(); assert!(debug); } #[tokio::test] async fn test_static_config_source() { let mut static_values = HashMap::new(); static_values.insert( "app.name".to_string(), ConfigValue::String("TestApp".to_string()), ); static_values.insert( "app.version".to_string(), ConfigValue::String("1.0.0".to_string()), ); let manager = ConfigManager::builder() .add_source(ConfigSource::Static { values: static_values, }) .build() .await .unwrap(); let app_name = manager.get::("app.name").await.unwrap(); assert_eq!(app_name, "TestApp"); let app_version = manager.get::("app.version").await.unwrap(); assert_eq!(app_version, "1.0.0"); } #[test] fn test_config_value_type_name() { assert_eq!( ConfigValue::String("test".to_string()).type_name(), "String" ); assert_eq!(ConfigValue::Integer(42).type_name(), "Integer"); assert_eq!(ConfigValue::Boolean(true).type_name(), "Boolean"); assert_eq!(ConfigValue::Null.type_name(), "Null"); } }