1use serde::{Deserialize, Serialize};
7use std::time::Duration;
8use elif_core::app_config::{AppConfigTrait, ConfigError, ConfigSource};
9use std::collections::HashMap;
10use std::env;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct HttpConfig {
15 pub request_timeout_secs: u64,
17 pub keep_alive_timeout_secs: u64,
19 pub max_request_size: usize,
21 pub enable_tracing: bool,
23 pub health_check_path: String,
25 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, 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 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 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 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 pub fn request_timeout(&self) -> Duration {
159 Duration::from_secs(self.request_timeout_secs)
160 }
161
162 pub fn keep_alive_timeout(&self) -> Duration {
164 Duration::from_secs(self.keep_alive_timeout_secs)
165 }
166
167 pub fn shutdown_timeout(&self) -> Duration {
169 Duration::from_secs(self.shutdown_timeout_secs)
170 }
171}
172
173fn 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 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"); 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 let mut invalid_config = config.clone();
243 invalid_config.request_timeout_secs = 0;
244 assert!(invalid_config.validate().is_err());
245
246 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}