1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct HttpConfig {
16 pub request_timeout_secs: u64,
18 pub keep_alive_timeout_secs: u64,
20 pub max_request_size: usize,
22 pub enable_tracing: bool,
24 pub health_check_path: String,
26 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 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 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 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 pub fn request_timeout(&self) -> Duration {
183 Duration::from_secs(self.request_timeout_secs)
184 }
185
186 pub fn keep_alive_timeout(&self) -> Duration {
188 Duration::from_secs(self.keep_alive_timeout_secs)
189 }
190
191 pub fn shutdown_timeout(&self) -> Duration {
193 Duration::from_secs(self.shutdown_timeout_secs)
194 }
195}
196
197fn 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 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"); 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}