442 lines
15 KiB
Rust
442 lines
15 KiB
Rust
//! Environment-specific configuration management.
|
|
|
|
use crate::{ConfigError, ConfigResult, ConfigValue};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use tracing::{debug, info};
|
|
|
|
/// Deployment environment types.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum Environment {
|
|
Development,
|
|
Testing,
|
|
Staging,
|
|
Production,
|
|
Custom(String),
|
|
}
|
|
|
|
impl Default for Environment {
|
|
fn default() -> Self {
|
|
// Try to detect from environment variable
|
|
if let Ok(env_str) = std::env::var("RTX_ENV") {
|
|
env_str.parse().unwrap_or(Environment::Development)
|
|
} else {
|
|
Environment::Development
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::str::FromStr for Environment {
|
|
type Err = ConfigError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s.to_lowercase().as_str() {
|
|
"development" | "dev" => Ok(Environment::Development),
|
|
"testing" | "test" => Ok(Environment::Testing),
|
|
"staging" | "stage" => Ok(Environment::Staging),
|
|
"production" | "prod" => Ok(Environment::Production),
|
|
other => Ok(Environment::Custom(other.to_string())),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for Environment {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Environment::Development => write!(f, "development"),
|
|
Environment::Testing => write!(f, "testing"),
|
|
Environment::Staging => write!(f, "staging"),
|
|
Environment::Production => write!(f, "production"),
|
|
Environment::Custom(name) => write!(f, "{}", name),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Environment-specific configuration.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct EnvironmentConfig {
|
|
/// Environment name
|
|
pub environment: Environment,
|
|
/// Environment-specific values
|
|
pub values: HashMap<String, ConfigValue>,
|
|
/// Inherit from base environment
|
|
pub inherit_from: Option<Environment>,
|
|
/// Environment description
|
|
pub description: Option<String>,
|
|
/// Environment-specific feature flags
|
|
pub features: HashMap<String, bool>,
|
|
}
|
|
|
|
impl EnvironmentConfig {
|
|
/// Create a new environment configuration.
|
|
pub fn new(environment: Environment) -> Self {
|
|
Self {
|
|
environment,
|
|
values: HashMap::new(),
|
|
inherit_from: None,
|
|
description: None,
|
|
features: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Set inheritance from another environment.
|
|
pub fn inherit_from(mut self, parent: Environment) -> Self {
|
|
self.inherit_from = Some(parent);
|
|
self
|
|
}
|
|
|
|
/// Add a configuration value.
|
|
pub fn with_value(mut self, key: impl Into<String>, value: ConfigValue) -> Self {
|
|
self.values.insert(key.into(), value);
|
|
self
|
|
}
|
|
|
|
/// Add a feature flag.
|
|
pub fn with_feature(mut self, feature: impl Into<String>, enabled: bool) -> Self {
|
|
self.features.insert(feature.into(), enabled);
|
|
self
|
|
}
|
|
|
|
/// Set description.
|
|
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
|
self.description = Some(description.into());
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Environment configuration manager.
|
|
#[derive(Debug)]
|
|
pub struct EnvironmentManager {
|
|
/// Current environment
|
|
current: Environment,
|
|
/// Environment configurations
|
|
configs: HashMap<Environment, EnvironmentConfig>,
|
|
}
|
|
|
|
impl EnvironmentManager {
|
|
/// Create a new environment manager.
|
|
pub fn new(current: Environment) -> Self {
|
|
let mut manager = Self {
|
|
current: current.clone(),
|
|
configs: HashMap::new(),
|
|
};
|
|
|
|
// Add default configurations
|
|
manager.add_default_configs();
|
|
|
|
info!("Environment manager initialized for: {}", current);
|
|
manager
|
|
}
|
|
|
|
/// Add an environment configuration.
|
|
pub fn add_config(&mut self, config: EnvironmentConfig) {
|
|
let env = config.environment.clone();
|
|
self.configs.insert(env.clone(), config);
|
|
debug!("Added configuration for environment: {}", env);
|
|
}
|
|
|
|
/// Get the current environment.
|
|
pub fn current(&self) -> &Environment {
|
|
&self.current
|
|
}
|
|
|
|
/// Switch to a different environment.
|
|
pub fn switch_to(&mut self, environment: Environment) -> ConfigResult<()> {
|
|
if !self.configs.contains_key(&environment) {
|
|
return Err(ConfigError::EnvironmentError {
|
|
name: environment.to_string(),
|
|
details: "Environment configuration not found".to_string(),
|
|
});
|
|
}
|
|
|
|
self.current = environment.clone();
|
|
info!("Switched to environment: {}", environment);
|
|
Ok(())
|
|
}
|
|
|
|
/// Get resolved configuration for current environment.
|
|
pub fn get_resolved_config(&self) -> ConfigResult<HashMap<String, ConfigValue>> {
|
|
self.resolve_environment_config(&self.current)
|
|
}
|
|
|
|
/// Get resolved configuration for a specific environment.
|
|
pub fn resolve_environment_config(
|
|
&self,
|
|
env: &Environment,
|
|
) -> ConfigResult<HashMap<String, ConfigValue>> {
|
|
let mut resolved_values = HashMap::new();
|
|
|
|
// Resolve configuration with inheritance
|
|
self.resolve_with_inheritance(
|
|
env,
|
|
&mut resolved_values,
|
|
&mut std::collections::HashSet::new(),
|
|
)?;
|
|
|
|
Ok(resolved_values)
|
|
}
|
|
|
|
/// Check if a feature is enabled in the current environment.
|
|
pub fn is_feature_enabled(&self, feature: &str) -> bool {
|
|
if let Ok(config) = self.get_current_config() {
|
|
config.features.get(feature).copied().unwrap_or(false)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Get all available environments.
|
|
pub fn available_environments(&self) -> Vec<&Environment> {
|
|
self.configs.keys().collect()
|
|
}
|
|
|
|
/// Get current environment configuration.
|
|
fn get_current_config(&self) -> ConfigResult<&EnvironmentConfig> {
|
|
self.configs
|
|
.get(&self.current)
|
|
.ok_or_else(|| ConfigError::EnvironmentError {
|
|
name: self.current.to_string(),
|
|
details: "Current environment configuration not found".to_string(),
|
|
})
|
|
}
|
|
|
|
/// Resolve configuration with inheritance chain.
|
|
fn resolve_with_inheritance(
|
|
&self,
|
|
env: &Environment,
|
|
resolved: &mut HashMap<String, ConfigValue>,
|
|
visited: &mut std::collections::HashSet<Environment>,
|
|
) -> ConfigResult<()> {
|
|
// Check for circular inheritance
|
|
if visited.contains(env) {
|
|
return Err(ConfigError::EnvironmentError {
|
|
name: env.to_string(),
|
|
details: "Circular inheritance detected".to_string(),
|
|
});
|
|
}
|
|
|
|
visited.insert(env.clone());
|
|
|
|
let config = self
|
|
.configs
|
|
.get(env)
|
|
.ok_or_else(|| ConfigError::EnvironmentError {
|
|
name: env.to_string(),
|
|
details: "Environment configuration not found".to_string(),
|
|
})?;
|
|
|
|
// First resolve parent configuration
|
|
if let Some(ref parent) = config.inherit_from {
|
|
self.resolve_with_inheritance(parent, resolved, visited)?;
|
|
}
|
|
|
|
// Then apply current environment values (overriding parent values)
|
|
for (key, value) in &config.values {
|
|
resolved.insert(key.clone(), value.clone());
|
|
}
|
|
|
|
visited.remove(env);
|
|
Ok(())
|
|
}
|
|
|
|
/// Add default environment configurations.
|
|
fn add_default_configs(&mut self) {
|
|
// Development environment
|
|
let dev_config = EnvironmentConfig::new(Environment::Development)
|
|
.with_description("Development environment with debug settings")
|
|
.with_value("log.level", ConfigValue::String("debug".to_string()))
|
|
.with_value("debug", ConfigValue::Boolean(true))
|
|
.with_value("metrics.enabled", ConfigValue::Boolean(false))
|
|
.with_feature("hot_reload", true)
|
|
.with_feature("debug_api", true);
|
|
|
|
// Testing environment
|
|
let test_config = EnvironmentConfig::new(Environment::Testing)
|
|
.with_description("Testing environment for automated tests")
|
|
.inherit_from(Environment::Development)
|
|
.with_value("log.level", ConfigValue::String("info".to_string()))
|
|
.with_value(
|
|
"database.url",
|
|
ConfigValue::String("sqlite::memory:".to_string()),
|
|
)
|
|
.with_feature("hot_reload", false)
|
|
.with_feature("test_mode", true);
|
|
|
|
// Staging environment
|
|
let staging_config = EnvironmentConfig::new(Environment::Staging)
|
|
.with_description("Staging environment for pre-production testing")
|
|
.with_value("log.level", ConfigValue::String("info".to_string()))
|
|
.with_value("debug", ConfigValue::Boolean(false))
|
|
.with_value("metrics.enabled", ConfigValue::Boolean(true))
|
|
.with_feature("hot_reload", false)
|
|
.with_feature("performance_monitoring", true);
|
|
|
|
// Production environment
|
|
let prod_config = EnvironmentConfig::new(Environment::Production)
|
|
.with_description("Production environment")
|
|
.with_value("log.level", ConfigValue::String("warn".to_string()))
|
|
.with_value("debug", ConfigValue::Boolean(false))
|
|
.with_value("metrics.enabled", ConfigValue::Boolean(true))
|
|
.with_value("security.strict_mode", ConfigValue::Boolean(true))
|
|
.with_feature("hot_reload", false)
|
|
.with_feature("performance_monitoring", true)
|
|
.with_feature("security_logging", true);
|
|
|
|
self.add_config(dev_config);
|
|
self.add_config(test_config);
|
|
self.add_config(staging_config);
|
|
self.add_config(prod_config);
|
|
}
|
|
}
|
|
|
|
impl Default for EnvironmentManager {
|
|
fn default() -> Self {
|
|
Self::new(Environment::default())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_environment_parsing() {
|
|
assert_eq!(
|
|
"development".parse::<Environment>().unwrap(),
|
|
Environment::Development
|
|
);
|
|
assert_eq!(
|
|
"dev".parse::<Environment>().unwrap(),
|
|
Environment::Development
|
|
);
|
|
assert_eq!(
|
|
"production".parse::<Environment>().unwrap(),
|
|
Environment::Production
|
|
);
|
|
assert_eq!(
|
|
"prod".parse::<Environment>().unwrap(),
|
|
Environment::Production
|
|
);
|
|
assert_eq!(
|
|
"custom".parse::<Environment>().unwrap(),
|
|
Environment::Custom("custom".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_environment_display() {
|
|
assert_eq!(Environment::Development.to_string(), "development");
|
|
assert_eq!(Environment::Production.to_string(), "production");
|
|
assert_eq!(
|
|
Environment::Custom("custom".to_string()).to_string(),
|
|
"custom"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_environment_config_creation() {
|
|
let config = EnvironmentConfig::new(Environment::Development)
|
|
.with_value("test.key", ConfigValue::String("test_value".to_string()))
|
|
.with_feature("test_feature", true)
|
|
.with_description("Test environment");
|
|
|
|
assert_eq!(config.environment, Environment::Development);
|
|
assert_eq!(
|
|
config.values.get("test.key").unwrap(),
|
|
&ConfigValue::String("test_value".to_string())
|
|
);
|
|
assert_eq!(config.features.get("test_feature").unwrap(), &true);
|
|
assert_eq!(config.description.as_ref().unwrap(), "Test environment");
|
|
}
|
|
|
|
#[test]
|
|
fn test_environment_manager_basic() {
|
|
let manager = EnvironmentManager::new(Environment::Development);
|
|
|
|
assert_eq!(manager.current(), &Environment::Development);
|
|
assert!(manager.available_environments().len() >= 4); // dev, test, staging, prod
|
|
}
|
|
|
|
#[test]
|
|
fn test_environment_inheritance() {
|
|
let mut manager = EnvironmentManager::new(Environment::Development);
|
|
|
|
// Create a custom environment that inherits from development
|
|
let custom_config = EnvironmentConfig::new(Environment::Custom("custom".to_string()))
|
|
.inherit_from(Environment::Development)
|
|
.with_value(
|
|
"custom.setting",
|
|
ConfigValue::String("custom_value".to_string()),
|
|
);
|
|
|
|
manager.add_config(custom_config);
|
|
|
|
let resolved = manager
|
|
.resolve_environment_config(&Environment::Custom("custom".to_string()))
|
|
.unwrap();
|
|
|
|
// Should have inherited values from development
|
|
assert!(resolved.contains_key("debug"));
|
|
assert!(resolved.contains_key("log.level"));
|
|
|
|
// Should have custom values
|
|
assert_eq!(
|
|
resolved.get("custom.setting").unwrap(),
|
|
&ConfigValue::String("custom_value".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_feature_flags() {
|
|
let manager = EnvironmentManager::new(Environment::Development);
|
|
|
|
assert!(manager.is_feature_enabled("hot_reload")); // Development should have this enabled
|
|
assert!(manager.is_feature_enabled("debug_api")); // Development should have this enabled
|
|
|
|
let mut prod_manager = EnvironmentManager::new(Environment::Production);
|
|
prod_manager.switch_to(Environment::Production).unwrap();
|
|
|
|
assert!(!prod_manager.is_feature_enabled("hot_reload")); // Production should not have this
|
|
assert!(prod_manager.is_feature_enabled("security_logging")); // Production should have this
|
|
}
|
|
|
|
#[test]
|
|
fn test_circular_inheritance_detection() {
|
|
let mut manager = EnvironmentManager::new(Environment::Development);
|
|
|
|
// Create circular inheritance: A -> B -> A
|
|
let env_a = Environment::Custom("a".to_string());
|
|
let env_b = Environment::Custom("b".to_string());
|
|
|
|
let config_a = EnvironmentConfig::new(env_a.clone()).inherit_from(env_b.clone());
|
|
|
|
let config_b = EnvironmentConfig::new(env_b.clone()).inherit_from(env_a.clone());
|
|
|
|
manager.add_config(config_a);
|
|
manager.add_config(config_b);
|
|
|
|
let result = manager.resolve_environment_config(&env_a);
|
|
assert!(result.is_err());
|
|
|
|
if let Err(ConfigError::EnvironmentError { details, .. }) = result {
|
|
assert!(details.contains("Circular inheritance"));
|
|
} else {
|
|
panic!("Expected circular inheritance error");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_environment_switching() {
|
|
let mut manager = EnvironmentManager::new(Environment::Development);
|
|
|
|
assert_eq!(manager.current(), &Environment::Development);
|
|
|
|
manager.switch_to(Environment::Production).unwrap();
|
|
assert_eq!(manager.current(), &Environment::Production);
|
|
|
|
// Test switching to non-existent environment
|
|
let result = manager.switch_to(Environment::Custom("nonexistent".to_string()));
|
|
assert!(result.is_err());
|
|
}
|
|
}
|