elif_http/
config.rs

1//! HTTP server configuration
2//! 
3//! Provides configuration structures for HTTP server setup, integrating with
4//! the elif-core configuration system.
5
6use serde::{Deserialize, Serialize};
7use std::time::Duration;
8use elif_core::app_config::{AppConfigTrait, ConfigError, ConfigSource};
9use std::collections::HashMap;
10use std::env;
11
12/// HTTP server specific configuration
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct HttpConfig {
15    /// Request timeout in seconds
16    pub request_timeout_secs: u64,
17    /// Keep alive timeout in seconds  
18    pub keep_alive_timeout_secs: u64,
19    /// Maximum request body size in bytes
20    pub max_request_size: usize,
21    /// Enable request tracing
22    pub enable_tracing: bool,
23    /// Health check endpoint path
24    pub health_check_path: String,
25    /// Server shutdown timeout in seconds
26    pub shutdown_timeout_secs: u64,
27}
28
29impl Default for HttpConfig {
30    fn default() -> Self {
31        Self {
32            request_timeout_secs: 30,
33            keep_alive_timeout_secs: 75,
34            max_request_size: 16 * 1024 * 1024, // 16MB
35            enable_tracing: true,
36            health_check_path: "/health".to_string(),
37            shutdown_timeout_secs: 10,
38        }
39    }
40}
41
42impl AppConfigTrait for HttpConfig {
43    fn from_env() -> Result<Self, ConfigError> {
44        let request_timeout_secs = get_env_or_default("HTTP_REQUEST_TIMEOUT", "30")?
45            .parse::<u64>()
46            .map_err(|_| ConfigError::InvalidValue {
47                field: "request_timeout_secs".to_string(),
48                value: env::var("HTTP_REQUEST_TIMEOUT").unwrap_or_default(),
49                expected: "valid number of seconds".to_string(),
50            })?;
51
52        let keep_alive_timeout_secs = get_env_or_default("HTTP_KEEP_ALIVE_TIMEOUT", "75")?
53            .parse::<u64>()
54            .map_err(|_| ConfigError::InvalidValue {
55                field: "keep_alive_timeout_secs".to_string(),
56                value: env::var("HTTP_KEEP_ALIVE_TIMEOUT").unwrap_or_default(),
57                expected: "valid number of seconds".to_string(),
58            })?;
59
60        let max_request_size = get_env_or_default("HTTP_MAX_REQUEST_SIZE", "16777216")?
61            .parse::<usize>()
62            .map_err(|_| ConfigError::InvalidValue {
63                field: "max_request_size".to_string(),
64                value: env::var("HTTP_MAX_REQUEST_SIZE").unwrap_or_default(),
65                expected: "valid number of bytes".to_string(),
66            })?;
67
68        let enable_tracing = get_env_or_default("HTTP_ENABLE_TRACING", "true")?
69            .parse::<bool>()
70            .map_err(|_| ConfigError::InvalidValue {
71                field: "enable_tracing".to_string(),
72                value: env::var("HTTP_ENABLE_TRACING").unwrap_or_default(),
73                expected: "true or false".to_string(),
74            })?;
75
76        let health_check_path = get_env_or_default("HTTP_HEALTH_CHECK_PATH", "/health")?;
77
78        let shutdown_timeout_secs = get_env_or_default("HTTP_SHUTDOWN_TIMEOUT", "10")?
79            .parse::<u64>()
80            .map_err(|_| ConfigError::InvalidValue {
81                field: "shutdown_timeout_secs".to_string(),
82                value: env::var("HTTP_SHUTDOWN_TIMEOUT").unwrap_or_default(),
83                expected: "valid number of seconds".to_string(),
84            })?;
85
86        Ok(HttpConfig {
87            request_timeout_secs,
88            keep_alive_timeout_secs,
89            max_request_size,
90            enable_tracing,
91            health_check_path,
92            shutdown_timeout_secs,
93        })
94    }
95
96    fn validate(&self) -> Result<(), ConfigError> {
97        // Validate timeout values
98        if self.request_timeout_secs == 0 {
99            return Err(ConfigError::ValidationFailed {
100                field: "request_timeout_secs".to_string(),
101                reason: "Request timeout must be greater than 0".to_string(),
102            });
103        }
104
105        if self.keep_alive_timeout_secs == 0 {
106            return Err(ConfigError::ValidationFailed {
107                field: "keep_alive_timeout_secs".to_string(),
108                reason: "Keep-alive timeout must be greater than 0".to_string(),
109            });
110        }
111
112        if self.shutdown_timeout_secs == 0 {
113            return Err(ConfigError::ValidationFailed {
114                field: "shutdown_timeout_secs".to_string(),
115                reason: "Shutdown timeout must be greater than 0".to_string(),
116            });
117        }
118
119        // Validate request size limits
120        if self.max_request_size == 0 {
121            return Err(ConfigError::ValidationFailed {
122                field: "max_request_size".to_string(),
123                reason: "Maximum request size must be greater than 0".to_string(),
124            });
125        }
126
127        // Validate health check path
128        if self.health_check_path.is_empty() || !self.health_check_path.starts_with('/') {
129            return Err(ConfigError::ValidationFailed {
130                field: "health_check_path".to_string(),
131                reason: "Health check path must be non-empty and start with '/'".to_string(),
132            });
133        }
134
135        Ok(())
136    }
137
138    fn config_sources(&self) -> HashMap<String, ConfigSource> {
139        let mut sources = HashMap::new();
140        sources.insert("request_timeout_secs".to_string(), 
141            ConfigSource::EnvVar("HTTP_REQUEST_TIMEOUT".to_string()));
142        sources.insert("keep_alive_timeout_secs".to_string(), 
143            ConfigSource::EnvVar("HTTP_KEEP_ALIVE_TIMEOUT".to_string()));
144        sources.insert("max_request_size".to_string(), 
145            ConfigSource::EnvVar("HTTP_MAX_REQUEST_SIZE".to_string()));
146        sources.insert("enable_tracing".to_string(), 
147            ConfigSource::EnvVar("HTTP_ENABLE_TRACING".to_string()));
148        sources.insert("health_check_path".to_string(), 
149            ConfigSource::EnvVar("HTTP_HEALTH_CHECK_PATH".to_string()));
150        sources.insert("shutdown_timeout_secs".to_string(), 
151            ConfigSource::EnvVar("HTTP_SHUTDOWN_TIMEOUT".to_string()));
152        sources
153    }
154}
155
156impl HttpConfig {
157    /// Get request timeout as Duration
158    pub fn request_timeout(&self) -> Duration {
159        Duration::from_secs(self.request_timeout_secs)
160    }
161
162    /// Get keep-alive timeout as Duration
163    pub fn keep_alive_timeout(&self) -> Duration {
164        Duration::from_secs(self.keep_alive_timeout_secs)
165    }
166
167    /// Get shutdown timeout as Duration
168    pub fn shutdown_timeout(&self) -> Duration {
169        Duration::from_secs(self.shutdown_timeout_secs)
170    }
171}
172
173// Helper function for environment variable handling
174fn get_env_or_default(key: &str, default: &str) -> Result<String, ConfigError> {
175    Ok(env::var(key).unwrap_or_else(|_| default.to_string()))
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use std::env;
182    use std::sync::Mutex;
183
184    // Global test lock to prevent concurrent environment modifications
185    static TEST_MUTEX: Mutex<()> = Mutex::new(());
186
187    fn set_test_env() {
188        env::set_var("HTTP_REQUEST_TIMEOUT", "60");
189        env::set_var("HTTP_KEEP_ALIVE_TIMEOUT", "120");
190        env::set_var("HTTP_MAX_REQUEST_SIZE", "33554432"); // 32MB
191        env::set_var("HTTP_ENABLE_TRACING", "false");
192        env::set_var("HTTP_HEALTH_CHECK_PATH", "/api/health");
193        env::set_var("HTTP_SHUTDOWN_TIMEOUT", "15");
194    }
195
196    fn clean_test_env() {
197        env::remove_var("HTTP_REQUEST_TIMEOUT");
198        env::remove_var("HTTP_KEEP_ALIVE_TIMEOUT");
199        env::remove_var("HTTP_MAX_REQUEST_SIZE");
200        env::remove_var("HTTP_ENABLE_TRACING");
201        env::remove_var("HTTP_HEALTH_CHECK_PATH");
202        env::remove_var("HTTP_SHUTDOWN_TIMEOUT");
203    }
204
205    #[test]
206    fn test_http_config_defaults() {
207        let config = HttpConfig::default();
208        
209        assert_eq!(config.request_timeout_secs, 30);
210        assert_eq!(config.keep_alive_timeout_secs, 75);
211        assert_eq!(config.max_request_size, 16 * 1024 * 1024);
212        assert!(config.enable_tracing);
213        assert_eq!(config.health_check_path, "/health");
214        assert_eq!(config.shutdown_timeout_secs, 10);
215    }
216
217    #[test]
218    fn test_http_config_from_env() {
219        let _guard = TEST_MUTEX.lock().unwrap();
220        set_test_env();
221
222        let config = HttpConfig::from_env().unwrap();
223
224        assert_eq!(config.request_timeout_secs, 60);
225        assert_eq!(config.keep_alive_timeout_secs, 120);
226        assert_eq!(config.max_request_size, 33554432);
227        assert!(!config.enable_tracing);
228        assert_eq!(config.health_check_path, "/api/health");
229        assert_eq!(config.shutdown_timeout_secs, 15);
230
231        clean_test_env();
232    }
233
234    #[test]
235    fn test_http_config_validation() {
236        let _guard = TEST_MUTEX.lock().unwrap();
237        
238        let config = HttpConfig::default();
239        assert!(config.validate().is_ok());
240
241        // Test invalid request timeout
242        let mut invalid_config = config.clone();
243        invalid_config.request_timeout_secs = 0;
244        assert!(invalid_config.validate().is_err());
245
246        // Test invalid health check path
247        let mut invalid_config = config.clone();
248        invalid_config.health_check_path = "no-slash".to_string();
249        assert!(invalid_config.validate().is_err());
250    }
251
252    #[test]
253    fn test_duration_helpers() {
254        let config = HttpConfig::default();
255        
256        assert_eq!(config.request_timeout(), Duration::from_secs(30));
257        assert_eq!(config.keep_alive_timeout(), Duration::from_secs(75));
258        assert_eq!(config.shutdown_timeout(), Duration::from_secs(10));
259    }
260}