elif_core/config/
app_config.rs

1use crate::config::{ConfigSource, ConfigError};
2use std::env;
3use std::str::FromStr;
4use std::collections::HashMap;
5
6/// Configuration trait for application configuration
7pub trait AppConfigTrait: Sized {
8    /// Load configuration from environment variables
9    fn from_env() -> Result<Self, ConfigError>;
10    
11    /// Validate the configuration
12    fn validate(&self) -> Result<(), ConfigError>;
13    
14    /// Get configuration source information for debugging
15    fn config_sources(&self) -> HashMap<String, ConfigSource>;
16}
17
18/// Environment enumeration
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum Environment {
21    Development,
22    Testing,
23    Production,
24}
25
26impl FromStr for Environment {
27    type Err = ConfigError;
28    
29    fn from_str(s: &str) -> Result<Self, Self::Err> {
30        match s.to_lowercase().as_str() {
31            "development" | "dev" => Ok(Environment::Development),
32            "testing" | "test" => Ok(Environment::Testing),
33            "production" | "prod" => Ok(Environment::Production),
34            _ => Err(ConfigError::InvalidValue {
35                field: "environment".to_string(),
36                value: s.to_string(),
37                expected: "development, testing, or production".to_string(),
38            }),
39        }
40    }
41}
42
43impl std::fmt::Display for Environment {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        let env_str = match self {
46            Environment::Development => "development",
47            Environment::Testing => "testing",
48            Environment::Production => "production",
49        };
50        write!(f, "{}", env_str)
51    }
52}
53
54impl Environment {
55    /// Check if environment is development
56    pub fn is_development(&self) -> bool {
57        matches!(self, Environment::Development)
58    }
59    
60    /// Check if environment is testing
61    pub fn is_testing(&self) -> bool {
62        matches!(self, Environment::Testing)
63    }
64    
65    /// Check if environment is production
66    pub fn is_production(&self) -> bool {
67        matches!(self, Environment::Production)
68    }
69    
70    /// Get debug mode status based on environment
71    pub fn debug_mode(&self) -> bool {
72        !self.is_production()
73    }
74}
75
76/// Default application configuration
77#[derive(Debug, Clone)]
78pub struct AppConfig {
79    pub environment: Environment,
80    pub debug: bool,
81    pub port: u16,
82    pub host: String,
83    pub database_url: Option<String>,
84    pub redis_url: Option<String>,
85    pub log_level: String,
86    pub secret_key: Option<String>,
87}
88
89impl AppConfig {
90    /// Create a new default configuration
91    pub fn new() -> Self {
92        Self {
93            environment: Environment::Development,
94            debug: true,
95            port: 3000,
96            host: "127.0.0.1".to_string(),
97            database_url: None,
98            redis_url: None,
99            log_level: "info".to_string(),
100            secret_key: None,
101        }
102    }
103    
104    /// Create configuration for development
105    pub fn development() -> Self {
106        Self {
107            environment: Environment::Development,
108            debug: true,
109            port: 3000,
110            host: "127.0.0.1".to_string(),
111            database_url: Some("postgres://localhost/elif_dev".to_string()),
112            redis_url: Some("redis://localhost:6379".to_string()),
113            log_level: "debug".to_string(),
114            secret_key: Some("dev_secret_key".to_string()),
115        }
116    }
117    
118    /// Create configuration for testing
119    pub fn testing() -> Self {
120        Self {
121            environment: Environment::Testing,
122            debug: true,
123            port: 0, // Random port for tests
124            host: "127.0.0.1".to_string(),
125            database_url: Some("postgres://localhost/elif_test".to_string()),
126            redis_url: Some("redis://localhost:6379/1".to_string()),
127            log_level: "warn".to_string(),
128            secret_key: Some("test_secret_key".to_string()),
129        }
130    }
131    
132    /// Create configuration for production
133    pub fn production() -> Self {
134        Self {
135            environment: Environment::Production,
136            debug: false,
137            port: 8080,
138            host: "0.0.0.0".to_string(),
139            database_url: None, // Must be provided via env
140            redis_url: None,    // Must be provided via env
141            log_level: "info".to_string(),
142            secret_key: None,   // Must be provided via env
143        }
144    }
145    
146    /// Get the bind address
147    pub fn bind_address(&self) -> String {
148        format!("{}:{}", self.host, self.port)
149    }
150    
151    /// Check if database is configured
152    pub fn has_database(&self) -> bool {
153        self.database_url.is_some()
154    }
155    
156    /// Check if Redis is configured
157    pub fn has_redis(&self) -> bool {
158        self.redis_url.is_some()
159    }
160}
161
162impl Default for AppConfig {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168impl AppConfigTrait for AppConfig {
169    fn from_env() -> Result<Self, ConfigError> {
170        let mut config = Self::new();
171        
172        // Environment
173        if let Ok(env_str) = env::var("ENVIRONMENT") {
174            config.environment = env_str.parse()?;
175        }
176        
177        // Debug mode (defaults based on environment if not set)
178        if let Ok(debug_str) = env::var("DEBUG") {
179            config.debug = debug_str.parse().unwrap_or(config.environment.debug_mode());
180        } else {
181            config.debug = config.environment.debug_mode();
182        }
183        
184        // Port
185        if let Ok(port_str) = env::var("PORT") {
186            config.port = port_str.parse().map_err(|_| ConfigError::InvalidValue {
187                field: "port".to_string(),
188                value: port_str,
189                expected: "valid port number (0-65535)".to_string(),
190            })?;
191        }
192        
193        // Host
194        if let Ok(host) = env::var("HOST") {
195            config.host = host;
196        }
197        
198        // Database URL
199        config.database_url = env::var("DATABASE_URL").ok();
200        
201        // Redis URL
202        config.redis_url = env::var("REDIS_URL").ok();
203        
204        // Log level
205        if let Ok(log_level) = env::var("LOG_LEVEL") {
206            config.log_level = log_level;
207        }
208        
209        // Secret key
210        config.secret_key = env::var("SECRET_KEY").ok();
211        
212        config.validate()?;
213        Ok(config)
214    }
215    
216    fn validate(&self) -> Result<(), ConfigError> {
217        // Port is u16, so it's automatically within valid range (0-65535)
218        // Only validate that it's not 0 if needed, except in testing environment
219        if !self.environment.is_testing() && self.port == 0 {
220            return Err(ConfigError::InvalidValue {
221                field: "port".to_string(),
222                value: self.port.to_string(),
223                expected: "port between 1 and 65535".to_string(),
224            });
225        }
226        
227        // Validate log level
228        let valid_levels = ["error", "warn", "info", "debug", "trace"];
229        if !valid_levels.contains(&self.log_level.as_str()) {
230            return Err(ConfigError::InvalidValue {
231                field: "log_level".to_string(),
232                value: self.log_level.clone(),
233                expected: format!("one of: {}", valid_levels.join(", ")),
234            });
235        }
236        
237        // Production environment validations
238        if self.environment.is_production() {
239            if self.secret_key.is_none() {
240                return Err(ConfigError::MissingRequired {
241                    field: "secret_key".to_string(),
242                    hint: "SECRET_KEY environment variable is required in production".to_string(),
243                });
244            }
245            
246            if self.debug {
247                return Err(ConfigError::InvalidValue {
248                    field: "debug".to_string(),
249                    value: "true".to_string(),
250                    expected: "false in production environment".to_string(),
251                });
252            }
253        }
254        
255        Ok(())
256    }
257    
258    fn config_sources(&self) -> HashMap<String, ConfigSource> {
259        let mut sources = HashMap::new();
260        
261        sources.insert("environment".to_string(), 
262            if env::var("ENVIRONMENT").is_ok() {
263                ConfigSource::EnvVar("ENVIRONMENT".to_string())
264            } else {
265                ConfigSource::Default("development".to_string())
266            });
267            
268        sources.insert("debug".to_string(),
269            if env::var("DEBUG").is_ok() {
270                ConfigSource::EnvVar("DEBUG".to_string())
271            } else {
272                ConfigSource::Default("based on environment".to_string())
273            });
274            
275        sources.insert("port".to_string(),
276            if env::var("PORT").is_ok() {
277                ConfigSource::EnvVar("PORT".to_string())
278            } else {
279                ConfigSource::Default("3000".to_string())
280            });
281            
282        // Add other fields...
283        
284        sources
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_port_validation_in_testing_environment() {
294        // Port 0 should be allowed in testing environment
295        let mut config = AppConfig::testing();
296        config.port = 0;
297        assert!(config.validate().is_ok(), "Port 0 should be allowed in testing environment");
298    }
299
300    #[test]
301    fn test_port_validation_in_non_testing_environment() {
302        // Port 0 should not be allowed in development environment
303        let mut config = AppConfig::development();
304        config.port = 0;
305        assert!(config.validate().is_err(), "Port 0 should not be allowed in development environment");
306        
307        // Port 0 should not be allowed in production environment
308        let mut config = AppConfig::production();
309        config.port = 0;
310        assert!(config.validate().is_err(), "Port 0 should not be allowed in production environment");
311    }
312
313    #[test]
314    fn test_valid_port_numbers() {
315        // Valid ports should work in all environments
316        let mut config = AppConfig::development();
317        config.port = 3000;
318        assert!(config.validate().is_ok());
319
320        config = AppConfig::testing();
321        config.port = 8080;
322        assert!(config.validate().is_ok());
323
324        config = AppConfig::production();
325        config.port = 443;
326        // Note: Production config may fail validation due to other requirements (secret key, etc.)
327        // We only care about the port validation here
328        let result = config.validate();
329        // Extract port-specific errors
330        if let Err(ConfigError::InvalidValue { field, .. }) = result {
331            assert_ne!(field, "port", "Port validation should not fail for valid port numbers");
332        }
333    }
334}