1use crate::{DEFAULT_API_URL, DEFAULT_SERVER_PORT, DEFAULT_TIMEOUT_SECS, DEFAULT_USER_AGENT};
4use config::{Config, ConfigError, Environment, File};
5use serde::{Deserialize, Serialize};
6use std::path::Path;
7use tracing::info;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AppConfig {
12 pub server: ServerConfig,
14
15 pub cache: CacheConfig,
17
18 pub logging: LoggingConfig,
20
21 pub rate_limiting: RateLimitConfig,
23
24 pub crates_io: CratesIoConfig,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ServerConfig {
31 #[serde(default = "default_port")]
33 pub port: u16,
34
35 #[serde(default = "default_host")]
37 pub host: String,
38
39 #[serde(default = "default_workers")]
41 pub workers: usize,
42
43 #[serde(default = "default_request_timeout")]
45 pub request_timeout: u64,
46
47 #[serde(default = "default_enable_cors")]
49 pub enable_cors: bool,
50
51 #[serde(default = "default_enable_tracing")]
53 pub enable_tracing: bool,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct CacheConfig {
59 #[serde(default = "default_cache_enabled")]
61 pub enabled: bool,
62
63 #[serde(default = "default_cache_ttl")]
65 pub ttl_seconds: u64,
66
67 #[serde(default = "default_cache_max_entries")]
69 pub max_entries: usize,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct LoggingConfig {
75 #[serde(default = "default_log_level")]
77 pub level: String,
78
79 #[serde(default = "default_log_format")]
81 pub format: String,
82
83 pub file: Option<String>,
85
86 #[serde(default = "default_structured_logging")]
88 pub structured: bool,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct RateLimitConfig {
94 #[serde(default = "default_requests_per_minute")]
96 pub requests_per_minute: u32,
97
98 #[serde(default = "default_burst_size")]
100 pub burst_size: u32,
101
102 #[serde(default = "default_rate_limiting_enabled")]
104 pub enabled: bool,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct CratesIoConfig {
110 #[serde(default = "default_api_url")]
112 pub api_url: String,
113
114 #[serde(default = "default_user_agent")]
116 pub user_agent: String,
117
118 #[serde(default = "default_api_timeout")]
120 pub timeout_seconds: u64,
121
122 #[serde(default = "default_max_concurrent")]
124 pub max_concurrent: usize,
125
126 #[serde(default = "default_retry_attempts")]
128 pub retry_attempts: u32,
129}
130
131fn default_port() -> u16 {
133 DEFAULT_SERVER_PORT
134}
135fn default_host() -> String {
136 "0.0.0.0".to_string()
137}
138fn default_workers() -> usize {
139 num_cpus::get()
140}
141fn default_request_timeout() -> u64 {
142 30
143}
144fn default_enable_cors() -> bool {
145 true
146}
147fn default_enable_tracing() -> bool {
148 true
149}
150
151fn default_cache_enabled() -> bool {
152 true
153}
154fn default_cache_ttl() -> u64 {
155 300
156}
157fn default_cache_max_entries() -> usize {
158 1000
159}
160
161fn default_log_level() -> String {
162 "info".to_string()
163}
164fn default_log_format() -> String {
165 "pretty".to_string()
166}
167fn default_structured_logging() -> bool {
168 false
169}
170
171fn default_requests_per_minute() -> u32 {
172 100
173}
174fn default_burst_size() -> u32 {
175 20
176}
177fn default_rate_limiting_enabled() -> bool {
178 false
179}
180
181fn default_api_url() -> String {
182 DEFAULT_API_URL.to_string()
183}
184fn default_user_agent() -> String {
185 DEFAULT_USER_AGENT.to_string()
186}
187fn default_api_timeout() -> u64 {
188 DEFAULT_TIMEOUT_SECS
189}
190fn default_max_concurrent() -> usize {
191 10
192}
193fn default_retry_attempts() -> u32 {
194 3
195}
196
197impl Default for AppConfig {
198 fn default() -> Self {
199 Self {
200 server: ServerConfig::default(),
201 cache: CacheConfig::default(),
202 logging: LoggingConfig::default(),
203 rate_limiting: RateLimitConfig::default(),
204 crates_io: CratesIoConfig::default(),
205 }
206 }
207}
208
209impl Default for ServerConfig {
210 fn default() -> Self {
211 Self {
212 port: default_port(),
213 host: default_host(),
214 workers: default_workers(),
215 request_timeout: default_request_timeout(),
216 enable_cors: default_enable_cors(),
217 enable_tracing: default_enable_tracing(),
218 }
219 }
220}
221
222impl Default for CacheConfig {
223 fn default() -> Self {
224 Self {
225 enabled: default_cache_enabled(),
226 ttl_seconds: default_cache_ttl(),
227 max_entries: default_cache_max_entries(),
228 }
229 }
230}
231
232impl Default for LoggingConfig {
233 fn default() -> Self {
234 Self {
235 level: default_log_level(),
236 format: default_log_format(),
237 file: None,
238 structured: default_structured_logging(),
239 }
240 }
241}
242
243impl Default for RateLimitConfig {
244 fn default() -> Self {
245 Self {
246 requests_per_minute: default_requests_per_minute(),
247 burst_size: default_burst_size(),
248 enabled: default_rate_limiting_enabled(),
249 }
250 }
251}
252
253impl Default for CratesIoConfig {
254 fn default() -> Self {
255 Self {
256 api_url: default_api_url(),
257 user_agent: default_user_agent(),
258 timeout_seconds: default_api_timeout(),
259 max_concurrent: default_max_concurrent(),
260 retry_attempts: default_retry_attempts(),
261 }
262 }
263}
264
265impl AppConfig {
266 pub fn load() -> Result<Self, ConfigError> {
268 Self::load_from_file(None::<std::path::PathBuf>)
269 }
270
271 pub fn load_from_file<P: AsRef<Path>>(config_file: Option<P>) -> Result<Self, ConfigError> {
273 let mut builder = Config::builder();
274
275 builder = builder.add_source(Config::try_from(&AppConfig::default())?);
277
278 if let Some(path) = config_file {
280 let path = path.as_ref();
281 if path.exists() {
282 info!("Loading configuration from: {}", path.display());
283 builder = builder.add_source(File::from(path));
284 }
285 }
286
287 builder = builder.add_source(
289 Environment::with_prefix("CRATE_CHECKER")
290 .separator("__")
291 .try_parsing(true),
292 );
293
294 builder.build()?.try_deserialize()
295 }
296
297 pub fn validate(&self) -> Result<(), String> {
299 if self.server.port == 0 {
300 return Err("Server port cannot be 0".to_string());
301 }
302
303 if self.server.workers == 0 {
304 return Err("Server workers cannot be 0".to_string());
305 }
306
307 if self.server.request_timeout == 0 {
308 return Err("Request timeout cannot be 0".to_string());
309 }
310
311 if self.cache.enabled && self.cache.max_entries == 0 {
312 return Err("Cache max entries cannot be 0 when caching is enabled".to_string());
313 }
314
315 if !["trace", "debug", "info", "warn", "error"].contains(&self.logging.level.as_str()) {
316 return Err(format!("Invalid log level: {}", self.logging.level));
317 }
318
319 if !["json", "pretty", "compact"].contains(&self.logging.format.as_str()) {
320 return Err(format!("Invalid log format: {}", self.logging.format));
321 }
322
323 if self.crates_io.timeout_seconds == 0 {
324 return Err("API timeout cannot be 0".to_string());
325 }
326
327 if self.crates_io.max_concurrent == 0 {
328 return Err("Max concurrent requests cannot be 0".to_string());
329 }
330
331 Ok(())
332 }
333
334 pub fn create_sample_config() -> String {
336 toml::to_string_pretty(&AppConfig::default())
337 .unwrap_or_else(|_| "# Failed to generate sample config".to_string())
338 }
339
340 pub fn bind_address(&self) -> String {
342 format!("{}:{}", self.server.host, self.server.port)
343 }
344}
345
346#[derive(Debug, Clone)]
348pub struct EnvironmentConfig {
349 pub is_development: bool,
350 pub is_production: bool,
351 pub is_test: bool,
352}
353
354impl EnvironmentConfig {
355 pub fn detect() -> Self {
356 let env = std::env::var("RUST_ENV")
357 .or_else(|_| std::env::var("ENVIRONMENT"))
358 .unwrap_or_else(|_| "development".to_string())
359 .to_lowercase();
360
361 Self {
362 is_development: env == "development" || env == "dev",
363 is_production: env == "production" || env == "prod",
364 is_test: env == "test" || env == "testing",
365 }
366 }
367
368 pub fn apply_overrides(&self, config: &mut AppConfig) {
370 if self.is_development {
371 config.logging.level = "debug".to_string();
372 config.cache.enabled = false;
373 config.rate_limiting.enabled = false;
374 } else if self.is_production {
375 config.logging.level = "info".to_string();
376 config.logging.structured = true;
377 config.cache.enabled = true;
378 config.rate_limiting.enabled = true;
379 } else if self.is_test {
380 config.logging.level = "warn".to_string();
381 config.cache.enabled = false;
382 config.rate_limiting.enabled = false;
383 }
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use std::io::Write;
391 use tempfile::NamedTempFile;
392
393 #[test]
394 fn test_default_config() {
395 let config = AppConfig::default();
396 assert_eq!(config.server.port, DEFAULT_SERVER_PORT);
397 assert_eq!(config.crates_io.api_url, DEFAULT_API_URL);
398 assert!(config.validate().is_ok());
399 }
400
401 #[test]
402 fn test_load_from_file() {
403 let mut temp_file = NamedTempFile::new().unwrap();
404 let temp_path = temp_file.path().with_extension("toml");
405
406 writeln!(
407 temp_file,
408 r#"
409[server]
410port = 8080
411host = "127.0.0.1"
412
413[logging]
414level = "debug"
415"#
416 )
417 .unwrap();
418
419 std::fs::copy(temp_file.path(), &temp_path).unwrap();
421
422 let config = AppConfig::load_from_file(Some(&temp_path)).unwrap();
423 assert_eq!(config.server.port, 8080);
424 assert_eq!(config.server.host, "127.0.0.1");
425 assert_eq!(config.logging.level, "debug");
426
427 std::fs::remove_file(&temp_path).ok();
429 }
430
431 #[test]
432 fn test_environment_overrides() {
433 let env_config = EnvironmentConfig {
434 is_development: true,
435 is_production: false,
436 is_test: false,
437 };
438
439 let mut config = AppConfig::default();
440 env_config.apply_overrides(&mut config);
441
442 assert_eq!(config.logging.level, "debug");
443 assert!(!config.cache.enabled);
444 assert!(!config.rate_limiting.enabled);
445 }
446
447 #[test]
448 fn test_bind_address() {
449 let config = AppConfig::default();
450 assert!(config
451 .bind_address()
452 .contains(&config.server.port.to_string()));
453 }
454
455 #[test]
456 fn test_create_sample_config() {
457 let sample = AppConfig::create_sample_config();
458 assert!(sample.contains("[server]"));
459 assert!(sample.contains("[logging]"));
460 assert!(sample.contains("[cache]"));
461 }
462}