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