elif_core/
app_config.rs

1use std::env;
2use std::str::FromStr;
3use std::collections::HashMap;
4use thiserror::Error;
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/// Configuration source information for debugging and hot-reload
19#[derive(Debug, Clone)]
20pub enum ConfigSource {
21    EnvVar(String),
22    Default(String),
23    Nested,
24}
25
26/// Environment enumeration
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Environment {
29    Development,
30    Testing,
31    Production,
32}
33
34impl FromStr for Environment {
35    type Err = ConfigError;
36    
37    fn from_str(s: &str) -> Result<Self, Self::Err> {
38        match s.to_lowercase().as_str() {
39            "development" | "dev" => Ok(Environment::Development),
40            "testing" | "test" => Ok(Environment::Testing),
41            "production" | "prod" => Ok(Environment::Production),
42            _ => Err(ConfigError::InvalidValue {
43                field: "environment".to_string(),
44                value: s.to_string(),
45                expected: "development, testing, or production".to_string(),
46            }),
47        }
48    }
49}
50
51impl Default for Environment {
52    fn default() -> Self {
53        Environment::Development
54    }
55}
56
57/// Application configuration structure
58#[derive(Debug, Clone)]
59pub struct AppConfig {
60    pub name: String,
61    pub environment: Environment,
62    pub database_url: String,
63    pub jwt_secret: Option<String>,
64    pub server: ServerConfig,
65    pub logging: LoggingConfig,
66}
67
68/// Server configuration
69#[derive(Debug, Clone)]
70pub struct ServerConfig {
71    pub host: String,
72    pub port: u16,
73    pub workers: usize,
74}
75
76/// Logging configuration
77#[derive(Debug, Clone)]
78pub struct LoggingConfig {
79    pub level: String,
80    pub format: String,
81}
82
83impl AppConfigTrait for AppConfig {
84    fn from_env() -> Result<Self, ConfigError> {
85        let name = get_env_or_default("APP_NAME", "elif-app")?;
86        let environment = get_env_or_default("APP_ENV", "development")?;
87        let environment = Environment::from_str(&environment)?;
88        
89        let database_url = get_env_required("DATABASE_URL")?;
90        let jwt_secret = get_env_optional("JWT_SECRET");
91        
92        let server = ServerConfig::from_env()?;
93        let logging = LoggingConfig::from_env()?;
94        
95        Ok(AppConfig {
96            name,
97            environment,
98            database_url,
99            jwt_secret,
100            server,
101            logging,
102        })
103    }
104    
105    fn validate(&self) -> Result<(), ConfigError> {
106        // Validate app name
107        if self.name.is_empty() {
108            return Err(ConfigError::ValidationFailed {
109                field: "name".to_string(),
110                reason: "App name cannot be empty".to_string(),
111            });
112        }
113        
114        // Validate database URL
115        if self.database_url.is_empty() {
116            return Err(ConfigError::ValidationFailed {
117                field: "database_url".to_string(),
118                reason: "Database URL cannot be empty".to_string(),
119            });
120        }
121        
122        // Validate JWT secret in production
123        if self.environment == Environment::Production && self.jwt_secret.is_none() {
124            return Err(ConfigError::ValidationFailed {
125                field: "jwt_secret".to_string(),
126                reason: "JWT secret is required in production".to_string(),
127            });
128        }
129        
130        // Validate nested configurations
131        self.server.validate()?;
132        self.logging.validate()?;
133        
134        Ok(())
135    }
136    
137    fn config_sources(&self) -> HashMap<String, ConfigSource> {
138        let mut sources = HashMap::new();
139        sources.insert("name".to_string(), 
140            ConfigSource::EnvVar("APP_NAME".to_string()));
141        sources.insert("environment".to_string(), 
142            ConfigSource::EnvVar("APP_ENV".to_string()));
143        sources.insert("database_url".to_string(), 
144            ConfigSource::EnvVar("DATABASE_URL".to_string()));
145        sources.insert("jwt_secret".to_string(), 
146            ConfigSource::EnvVar("JWT_SECRET".to_string()));
147        sources.insert("server".to_string(), 
148            ConfigSource::Nested);
149        sources.insert("logging".to_string(), 
150            ConfigSource::Nested);
151        sources
152    }
153}
154
155impl AppConfigTrait for ServerConfig {
156    fn from_env() -> Result<Self, ConfigError> {
157        let host = get_env_or_default("SERVER_HOST", "0.0.0.0")?;
158        let port = get_env_or_default("SERVER_PORT", "3000")?;
159        let port = port.parse::<u16>()
160            .map_err(|_| ConfigError::InvalidValue {
161                field: "port".to_string(),
162                value: port,
163                expected: "valid port number (0-65535)".to_string(),
164            })?;
165            
166        let workers = get_env_or_default("SERVER_WORKERS", "0")?;
167        let workers = workers.parse::<usize>()
168            .map_err(|_| ConfigError::InvalidValue {
169                field: "workers".to_string(),
170                value: workers,
171                expected: "valid number".to_string(),
172            })?;
173        
174        // Auto-detect workers if 0
175        let workers = if workers == 0 {
176            num_cpus::get()
177        } else {
178            workers
179        };
180        
181        Ok(ServerConfig {
182            host,
183            port,
184            workers,
185        })
186    }
187    
188    fn validate(&self) -> Result<(), ConfigError> {
189        // Validate host
190        if self.host.is_empty() {
191            return Err(ConfigError::ValidationFailed {
192                field: "host".to_string(),
193                reason: "Host cannot be empty".to_string(),
194            });
195        }
196        
197        // Validate port range
198        if self.port == 0 {
199            return Err(ConfigError::ValidationFailed {
200                field: "port".to_string(),
201                reason: "Port cannot be 0".to_string(),
202            });
203        }
204        
205        // Validate workers
206        if self.workers == 0 {
207            return Err(ConfigError::ValidationFailed {
208                field: "workers".to_string(),
209                reason: "Workers cannot be 0 after auto-detection".to_string(),
210            });
211        }
212        
213        Ok(())
214    }
215    
216    fn config_sources(&self) -> HashMap<String, ConfigSource> {
217        let mut sources = HashMap::new();
218        sources.insert("host".to_string(), 
219            ConfigSource::EnvVar("SERVER_HOST".to_string()));
220        sources.insert("port".to_string(), 
221            ConfigSource::EnvVar("SERVER_PORT".to_string()));
222        sources.insert("workers".to_string(), 
223            ConfigSource::EnvVar("SERVER_WORKERS".to_string()));
224        sources
225    }
226}
227
228impl AppConfigTrait for LoggingConfig {
229    fn from_env() -> Result<Self, ConfigError> {
230        let level = get_env_or_default("LOG_LEVEL", "info")?;
231        let format = get_env_or_default("LOG_FORMAT", "compact")?;
232        
233        Ok(LoggingConfig {
234            level,
235            format,
236        })
237    }
238    
239    fn validate(&self) -> Result<(), ConfigError> {
240        // Validate log level
241        let valid_levels = ["trace", "debug", "info", "warn", "error"];
242        if !valid_levels.contains(&self.level.to_lowercase().as_str()) {
243            return Err(ConfigError::InvalidValue {
244                field: "level".to_string(),
245                value: self.level.clone(),
246                expected: "trace, debug, info, warn, or error".to_string(),
247            });
248        }
249        
250        // Validate log format
251        let valid_formats = ["compact", "pretty", "json"];
252        if !valid_formats.contains(&self.format.to_lowercase().as_str()) {
253            return Err(ConfigError::InvalidValue {
254                field: "format".to_string(),
255                value: self.format.clone(),
256                expected: "compact, pretty, or json".to_string(),
257            });
258        }
259        
260        Ok(())
261    }
262    
263    fn config_sources(&self) -> HashMap<String, ConfigSource> {
264        let mut sources = HashMap::new();
265        sources.insert("level".to_string(), 
266            ConfigSource::EnvVar("LOG_LEVEL".to_string()));
267        sources.insert("format".to_string(), 
268            ConfigSource::EnvVar("LOG_FORMAT".to_string()));
269        sources
270    }
271}
272
273/// Configuration hot-reload system for development
274pub struct ConfigWatcher {
275    config: AppConfig,
276    last_check: std::time::Instant,
277    check_interval: std::time::Duration,
278}
279
280impl ConfigWatcher {
281    pub fn new(config: AppConfig) -> Self {
282        Self {
283            config,
284            last_check: std::time::Instant::now(),
285            check_interval: std::time::Duration::from_secs(1),
286        }
287    }
288    
289    /// Check for configuration changes (for development hot-reload)
290    pub fn check_for_changes(&mut self) -> Result<Option<AppConfig>, ConfigError> {
291        let now = std::time::Instant::now();
292        if now.duration_since(self.last_check) < self.check_interval {
293            return Ok(None);
294        }
295        
296        self.last_check = now;
297        
298        // In development mode, reload from environment
299        if self.config.environment == Environment::Development {
300            let new_config = AppConfig::from_env()?;
301            new_config.validate()?;
302            
303            // Simple comparison - in a real implementation, you'd want more sophisticated change detection
304            let changed = format!("{:?}", new_config) != format!("{:?}", self.config);
305            
306            if changed {
307                self.config = new_config.clone();
308                return Ok(Some(new_config));
309            }
310        }
311        
312        Ok(None)
313    }
314    
315    /// Get current configuration
316    pub fn config(&self) -> &AppConfig {
317        &self.config
318    }
319}
320
321// Helper functions for environment variable handling
322fn get_env_required(key: &str) -> Result<String, ConfigError> {
323    env::var(key).map_err(|_| ConfigError::MissingEnvVar {
324        var: key.to_string(),
325    })
326}
327
328fn get_env_optional(key: &str) -> Option<String> {
329    env::var(key).ok()
330}
331
332fn get_env_or_default(key: &str, default: &str) -> Result<String, ConfigError> {
333    Ok(env::var(key).unwrap_or_else(|_| default.to_string()))
334}
335
336#[derive(Error, Debug)]
337pub enum ConfigError {
338    #[error("Missing required environment variable: {var}")]
339    MissingEnvVar { var: String },
340    
341    #[error("Invalid value for {field}: '{value}', expected {expected}")]
342    InvalidValue { field: String, value: String, expected: String },
343    
344    #[error("Validation failed for {field}: {reason}")]
345    ValidationFailed { field: String, reason: String },
346    
347    #[error("Configuration parsing error: {message}")]
348    ParseError { message: String },
349    
350    #[error("Configuration reload error: {message}")]
351    ReloadError { message: String },
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use std::env;
358    use std::sync::Mutex;
359    
360    // Global test lock to prevent concurrent environment modifications
361    static TEST_MUTEX: Mutex<()> = Mutex::new(());
362    
363    // Helper function to set test environment variables
364    fn set_test_env() {
365        env::set_var("APP_NAME", "test-app");
366        env::set_var("APP_ENV", "testing");
367        env::set_var("DATABASE_URL", "sqlite::memory:");
368        env::set_var("JWT_SECRET", "test-secret-key");
369        env::set_var("SERVER_HOST", "127.0.0.1");
370        env::set_var("SERVER_PORT", "8080");
371        env::set_var("SERVER_WORKERS", "4");
372        env::set_var("LOG_LEVEL", "debug");
373        env::set_var("LOG_FORMAT", "pretty");
374    }
375    
376    fn clean_test_env() {
377        env::remove_var("APP_NAME");
378        env::remove_var("APP_ENV");
379        env::remove_var("DATABASE_URL");
380        env::remove_var("JWT_SECRET");
381        env::remove_var("SERVER_HOST");
382        env::remove_var("SERVER_PORT");
383        env::remove_var("SERVER_WORKERS");
384        env::remove_var("LOG_LEVEL");
385        env::remove_var("LOG_FORMAT");
386    }
387    
388    #[test]
389    fn test_app_config_from_env() {
390        let _guard = TEST_MUTEX.lock().unwrap();
391        set_test_env();
392        
393        let config = AppConfig::from_env().unwrap();
394        
395        assert_eq!(config.name, "test-app");
396        assert_eq!(config.environment, Environment::Testing);
397        assert_eq!(config.database_url, "sqlite::memory:");
398        assert_eq!(config.jwt_secret, Some("test-secret-key".to_string()));
399        assert_eq!(config.server.host, "127.0.0.1");
400        assert_eq!(config.server.port, 8080);
401        assert_eq!(config.server.workers, 4);
402        assert_eq!(config.logging.level, "debug");
403        assert_eq!(config.logging.format, "pretty");
404        
405        clean_test_env();
406    }
407    
408    #[test]
409    fn test_app_config_defaults() {
410        let _guard = TEST_MUTEX.lock().unwrap();
411        clean_test_env();
412        env::set_var("DATABASE_URL", "sqlite::memory:");
413        
414        let config = AppConfig::from_env().unwrap();
415        
416        assert_eq!(config.name, "elif-app");
417        assert_eq!(config.environment, Environment::Development);
418        assert_eq!(config.server.host, "0.0.0.0");
419        assert_eq!(config.server.port, 3000);
420        assert_eq!(config.logging.level, "info");
421        assert_eq!(config.logging.format, "compact");
422        
423        clean_test_env();
424    }
425    
426    #[test]
427    fn test_missing_required_env_var() {
428        let _guard = TEST_MUTEX.lock().unwrap();
429        clean_test_env();
430        // Don't set DATABASE_URL
431        
432        let result = AppConfig::from_env();
433        assert!(result.is_err());
434        
435        if let Err(ConfigError::MissingEnvVar { var }) = result {
436            assert_eq!(var, "DATABASE_URL");
437        } else {
438            panic!("Expected MissingEnvVar error");
439        }
440    }
441    
442    #[test]
443    fn test_config_validation() {
444        let _guard = TEST_MUTEX.lock().unwrap();
445        set_test_env();
446        
447        let config = AppConfig::from_env().unwrap();
448        assert!(config.validate().is_ok());
449        
450        clean_test_env();
451    }
452    
453    #[test]
454    fn test_production_jwt_secret_validation() {
455        let _guard = TEST_MUTEX.lock().unwrap();
456        set_test_env();
457        env::set_var("APP_ENV", "production");
458        env::remove_var("JWT_SECRET");
459        
460        let config = AppConfig::from_env().unwrap();
461        let result = config.validate();
462        
463        assert!(result.is_err());
464        if let Err(ConfigError::ValidationFailed { field, .. }) = result {
465            assert_eq!(field, "jwt_secret");
466        } else {
467            panic!("Expected ValidationFailed error for jwt_secret");
468        }
469        
470        clean_test_env();
471    }
472    
473    #[test]
474    fn test_invalid_port() {
475        let _guard = TEST_MUTEX.lock().unwrap();
476        set_test_env();
477        env::set_var("SERVER_PORT", "invalid");
478        
479        let result = AppConfig::from_env();
480        assert!(result.is_err());
481        
482        if let Err(ConfigError::InvalidValue { field, .. }) = result {
483            assert_eq!(field, "port");
484        } else {
485            panic!("Expected InvalidValue error for port");
486        }
487        
488        clean_test_env();
489    }
490    
491    #[test]
492    fn test_invalid_log_level() {
493        let _guard = TEST_MUTEX.lock().unwrap();
494        set_test_env();
495        env::set_var("LOG_LEVEL", "invalid");
496        
497        let config = AppConfig::from_env().unwrap();
498        let result = config.validate();
499        
500        assert!(result.is_err());
501        if let Err(ConfigError::InvalidValue { field, .. }) = result {
502            assert_eq!(field, "level");
503        } else {
504            panic!("Expected InvalidValue error for log level");
505        }
506        
507        clean_test_env();
508    }
509    
510    #[test]
511    fn test_environment_parsing() {
512        assert_eq!(Environment::from_str("development").unwrap(), Environment::Development);
513        assert_eq!(Environment::from_str("dev").unwrap(), Environment::Development);
514        assert_eq!(Environment::from_str("testing").unwrap(), Environment::Testing);
515        assert_eq!(Environment::from_str("test").unwrap(), Environment::Testing);
516        assert_eq!(Environment::from_str("production").unwrap(), Environment::Production);
517        assert_eq!(Environment::from_str("prod").unwrap(), Environment::Production);
518        
519        assert!(Environment::from_str("invalid").is_err());
520    }
521    
522    #[test]
523    fn test_config_sources() {
524        let _guard = TEST_MUTEX.lock().unwrap();
525        set_test_env();
526        
527        let config = AppConfig::from_env().unwrap();
528        let sources = config.config_sources();
529        
530        assert!(matches!(sources.get("name"), Some(ConfigSource::EnvVar(_))));
531        assert!(matches!(sources.get("server"), Some(ConfigSource::Nested)));
532        
533        clean_test_env();
534    }
535    
536    #[test]
537    fn test_config_watcher() {
538        let _guard = TEST_MUTEX.lock().unwrap();
539        set_test_env();
540        
541        let config = AppConfig::from_env().unwrap();
542        let mut watcher = ConfigWatcher::new(config);
543        
544        // Check that no changes are detected immediately
545        let result = watcher.check_for_changes().unwrap();
546        assert!(result.is_none());
547        
548        clean_test_env();
549    }
550}