crates_docs/config/mod.rs
1//! Configuration module
2//!
3//! Provides application configuration management, supports loading from files, environment variables, and default values.
4//!
5//! # Configuration Source Priority
6//!
7//! 1. Environment variables (highest priority)
8//! 2. Configuration file
9//! 3. Default values (lowest priority)
10//!
11//! # Supported Configuration Formats
12//!
13//! - TOML configuration file
14//! - Environment variables (prefix `CRATES_DOCS_`)
15//!
16//! # Examples
17//!
18//! ```rust,no_run
19//! use crates_docs::config::AppConfig;
20//!
21//! // Load configuration from file
22//! let config = AppConfig::from_file("config.toml").expect("Failed to load config");
23//!
24//! // Load configuration from environment variables
25//! let config = AppConfig::from_env().expect("Failed to load config from env");
26//!
27//! // Use default configuration
28//! let config = AppConfig::default();
29//! ```
30
31use crate::cache::CacheConfig;
32use crate::server::auth::{AuthConfig, OAuthConfig};
33use rust_mcp_sdk::schema::{Icon, IconTheme};
34use serde::{Deserialize, Serialize};
35use std::fs;
36use std::path::Path;
37
38// HTTP Client defaults
39
40/// Default HTTP client connection pool size (10 connections)
41const DEFAULT_HTTP_CLIENT_POOL_SIZE: usize = 10;
42/// Default HTTP client pool idle timeout in seconds (90 seconds)
43const DEFAULT_HTTP_CLIENT_POOL_IDLE_TIMEOUT_SECS: u64 = 90;
44/// Default HTTP client connection timeout in seconds (10 seconds)
45const DEFAULT_HTTP_CLIENT_CONNECT_TIMEOUT_SECS: u64 = 10;
46/// Default HTTP client request timeout in seconds (30 seconds)
47const DEFAULT_HTTP_CLIENT_TIMEOUT_SECS: u64 = 30;
48/// Default HTTP client read timeout in seconds (30 seconds)
49const DEFAULT_HTTP_CLIENT_READ_TIMEOUT_SECS: u64 = 30;
50/// Default HTTP client max retry attempts (3 retries)
51const DEFAULT_HTTP_CLIENT_MAX_RETRIES: u32 = 3;
52/// Default HTTP client retry initial delay in milliseconds (100ms)
53const DEFAULT_HTTP_CLIENT_RETRY_INITIAL_DELAY_MS: u64 = 100;
54/// Default HTTP client retry max delay in milliseconds (10 seconds)
55const DEFAULT_HTTP_CLIENT_RETRY_MAX_DELAY_MS: u64 = 10_000;
56
57// Server defaults
58
59/// Default server port (8080)
60const DEFAULT_SERVER_PORT: u16 = 8080;
61/// Default server max concurrent connections (100 connections)
62const DEFAULT_SERVER_MAX_CONNECTIONS: usize = 100;
63/// Default request timeout in seconds (30 seconds)
64const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30;
65/// Default response timeout in seconds (60 seconds)
66const DEFAULT_RESPONSE_TIMEOUT_SECS: u64 = 60;
67
68// Cache/Rate limit defaults
69
70/// Default cache max size in number of entries (1000 entries)
71const DEFAULT_CACHE_MAX_SIZE: usize = 1000;
72/// Default cache TTL in seconds (1 hour = 3600 seconds)
73const DEFAULT_CACHE_DEFAULT_TTL_SECS: u64 = 3600;
74/// Default rate limit per second (100 requests)
75const DEFAULT_RATE_LIMIT_PER_SECOND: u32 = 100;
76/// Default concurrent request limit (50 requests)
77const DEFAULT_CONCURRENT_REQUEST_LIMIT: usize = 50;
78
79// File upload defaults
80
81/// Default max log file size in MB (100 MB)
82const DEFAULT_MAX_FILE_SIZE_MB: u64 = 100;
83/// Default number of log files to retain (10 files)
84const DEFAULT_MAX_FILES: usize = 10;
85
86/// Application configuration
87///
88/// Contains server, cache, authentication, logging, and performance configuration.
89///
90/// # Fields
91///
92/// - `server`: Server configuration
93/// - `cache`: Cache configuration
94/// - `auth`: Authentication configuration (OAuth and API Key)
95/// - `logging`: Logging configuration
96/// - `performance`: Performance configuration
97///
98/// # Hot Reload Support
99///
100/// The following configuration items support hot reload (runtime update without restart):
101/// - `logging` section: All fields
102/// - `auth` section: All fields (including API Key and OAuth)
103/// - `cache` section: TTL-related fields (`default_ttl`, `crate_docs_ttl_secs`, `item_docs_ttl_secs`, `search_results_ttl_secs`)
104/// - `performance` section: `rate_limit_per_second`, `concurrent_request_limit`, `enable_metrics`, `enable_response_compression`
105///
106/// The following configuration items **do not** support hot reload (require server restart):
107/// - `server` section: All fields (host, port, `transport_mode`, `max_connections`, etc.)
108/// - `cache` section: `cache_type`, `memory_size`, `redis_url` (cache initialization parameters)
109/// - `performance` section: `http_client_*`, `cache_max_size`, `cache_default_ttl_secs`, `metrics_port`
110#[derive(Debug, Clone, Deserialize, Serialize, Default)]
111pub struct AppConfig {
112 /// Server configuration
113 pub server: ServerConfig,
114
115 /// Cache configuration
116 pub cache: CacheConfig,
117
118 /// Authentication configuration (OAuth and API Key)
119 #[serde(default)]
120 pub auth: AuthConfig,
121
122 /// OAuth configuration (backwards compatible, prefer using auth.oauth)
123 #[serde(default)]
124 pub oauth: OAuthConfig,
125
126 /// Logging configuration
127 pub logging: LoggingConfig,
128
129 /// Performance configuration
130 pub performance: PerformanceConfig,
131}
132
133/// Server configuration
134///
135/// # Hot Reload Support
136///
137/// ⚠️ **Does not support hot reload** - Server configuration changes require server restart to take effect.
138///
139/// Reason: These configurations involve server listening socket, transport layer initialization and other core parameters,
140/// runtime changes may cause connection interruption or state inconsistency.
141#[derive(Debug, Clone, Deserialize, Serialize)]
142pub struct ServerConfig {
143 /// Server name
144 pub name: String,
145
146 /// Server version
147 #[serde(default = "default_version")]
148 pub version: String,
149
150 /// Server description
151 pub description: Option<String>,
152
153 /// Server icons
154 #[serde(default = "default_icons")]
155 pub icons: Vec<Icon>,
156
157 /// Website URL
158 pub website_url: Option<String>,
159
160 /// Host address
161 pub host: String,
162
163 /// Port
164 pub port: u16,
165
166 /// Transport mode
167 pub transport_mode: String,
168
169 /// Enable SSE support
170 pub enable_sse: bool,
171
172 /// Enable OAuth authentication
173 pub enable_oauth: bool,
174
175 /// Maximum concurrent connections
176 pub max_connections: usize,
177
178 /// Request timeout (seconds)
179 pub request_timeout_secs: u64,
180
181 /// Response timeout (seconds)
182 pub response_timeout_secs: u64,
183
184 /// Allowed hosts for CORS (e.g., `["localhost", "127.0.0.1"]`)
185 pub allowed_hosts: Vec<String>,
186
187 /// Allowed origins for CORS (e.g., `["http://localhost:*"]`)
188 /// Use `"*"` only in development, specify exact origins in production
189 pub allowed_origins: Vec<String>,
190}
191
192/// Default server version from Cargo.toml
193fn default_version() -> String {
194 crate::VERSION.to_string()
195}
196
197/// Default icons for the server
198fn default_icons() -> Vec<Icon> {
199 vec![
200 Icon {
201 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
202 mime_type: Some("image/png".to_string()),
203 sizes: vec!["32x32".to_string()],
204 theme: Some(IconTheme::Light),
205 },
206 Icon {
207 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
208 mime_type: Some("image/png".to_string()),
209 sizes: vec!["32x32".to_string()],
210 theme: Some(IconTheme::Dark),
211 },
212 ]
213}
214
215/// Logging configuration
216///
217/// # Hot Reload Support
218///
219/// ✅ **Supports hot reload** - All logging configuration items can be dynamically updated at runtime.
220///
221/// Hot reload supported fields:
222/// - `level`: Log level (trace/debug/info/warn/error)
223/// - `file_path`: Log file path
224/// - `enable_console`: Console logging toggle
225/// - `enable_file`: File logging toggle
226/// - `max_file_size_mb`: Maximum log file size
227/// - `max_files`: Number of log files to retain
228///
229/// Note: After file logging path changes, new logs will be written to the new file, but old file handles will not be automatically closed.
230#[derive(Debug, Clone, Deserialize, Serialize)]
231pub struct LoggingConfig {
232 /// Log level
233 pub level: String,
234
235 /// Log file path
236 pub file_path: Option<String>,
237
238 /// Whether to enable console logging
239 pub enable_console: bool,
240
241 /// Whether to enable file logging
242 pub enable_file: bool,
243
244 /// Maximum log file size (MB)
245 pub max_file_size_mb: u64,
246
247 /// Number of log files to retain
248 pub max_files: usize,
249}
250
251/// Performance configuration
252///
253/// # Hot Reload Support
254///
255/// ## Hot reload supported fields ✅
256///
257/// The following fields can be dynamically updated at runtime:
258/// - `rate_limit_per_second`: Request rate limit (requests per second)
259/// - `concurrent_request_limit`: Concurrent request limit
260/// - `enable_metrics`: Prometheus metrics collection toggle
261/// - `enable_response_compression`: Response compression toggle
262///
263/// ## Hot reload not supported fields ❌
264///
265/// The following fields require server restart to take effect:
266/// - `http_client_*`: HTTP client configuration (pool size, timeouts, etc.)
267/// - `cache_max_size`: Cache maximum size
268/// - `cache_default_ttl_secs`: Cache default TTL
269/// - `metrics_port`: Metrics server port
270///
271/// Reason: These configurations involve underlying connection pool, cache instance initialization parameters.
272#[derive(Debug, Clone, Deserialize, Serialize)]
273pub struct PerformanceConfig {
274 /// HTTP client connection pool size
275 pub http_client_pool_size: usize,
276
277 /// HTTP client pool idle timeout (seconds)
278 pub http_client_pool_idle_timeout_secs: u64,
279
280 /// HTTP client connection timeout (seconds)
281 pub http_client_connect_timeout_secs: u64,
282
283 /// HTTP client request timeout (seconds)
284 pub http_client_timeout_secs: u64,
285
286 /// HTTP client read timeout (seconds)
287 pub http_client_read_timeout_secs: u64,
288
289 /// HTTP client max retry attempts
290 pub http_client_max_retries: u32,
291
292 /// HTTP client retry initial delay (milliseconds)
293 pub http_client_retry_initial_delay_ms: u64,
294
295 /// HTTP client retry max delay (milliseconds)
296 pub http_client_retry_max_delay_ms: u64,
297
298 /// Maximum cache size (number of entries)
299 pub cache_max_size: usize,
300
301 /// Default cache TTL (seconds)
302 pub cache_default_ttl_secs: u64,
303
304 /// Request rate limit (requests per second)
305 pub rate_limit_per_second: u32,
306
307 /// Concurrent request limit
308 pub concurrent_request_limit: usize,
309
310 /// Enable response compression
311 pub enable_response_compression: bool,
312
313 /// Enable Prometheus metrics
314 pub enable_metrics: bool,
315
316 /// Metrics endpoint port (0 = use server port)
317 pub metrics_port: u16,
318}
319
320impl Default for ServerConfig {
321 fn default() -> Self {
322 Self {
323 name: "crates-docs".to_string(),
324 version: crate::VERSION.to_string(),
325 description: Some(
326 "High-performance Rust crate documentation query MCP server".to_string(),
327 ),
328 icons: default_icons(),
329 website_url: Some("https://github.com/KingingWang/crates-docs".to_string()),
330 host: "127.0.0.1".to_string(),
331 port: DEFAULT_SERVER_PORT,
332 transport_mode: "hybrid".to_string(),
333 enable_sse: true,
334 enable_oauth: false,
335 max_connections: DEFAULT_SERVER_MAX_CONNECTIONS,
336 request_timeout_secs: DEFAULT_REQUEST_TIMEOUT_SECS,
337 response_timeout_secs: DEFAULT_RESPONSE_TIMEOUT_SECS,
338 // Secure defaults: only allow localhost by default
339 allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
340 allowed_origins: vec!["http://localhost:*".to_string()],
341 }
342 }
343}
344
345impl Default for LoggingConfig {
346 fn default() -> Self {
347 Self {
348 level: "info".to_string(),
349 file_path: Some("./logs/crates-docs.log".to_string()),
350 enable_console: true,
351 enable_file: false, // Default: console output only
352 max_file_size_mb: DEFAULT_MAX_FILE_SIZE_MB,
353 max_files: DEFAULT_MAX_FILES,
354 }
355 }
356}
357
358impl Default for PerformanceConfig {
359 fn default() -> Self {
360 Self {
361 http_client_pool_size: DEFAULT_HTTP_CLIENT_POOL_SIZE,
362 http_client_pool_idle_timeout_secs: DEFAULT_HTTP_CLIENT_POOL_IDLE_TIMEOUT_SECS,
363 http_client_connect_timeout_secs: DEFAULT_HTTP_CLIENT_CONNECT_TIMEOUT_SECS,
364 http_client_timeout_secs: DEFAULT_HTTP_CLIENT_TIMEOUT_SECS,
365 http_client_read_timeout_secs: DEFAULT_HTTP_CLIENT_READ_TIMEOUT_SECS,
366 http_client_max_retries: DEFAULT_HTTP_CLIENT_MAX_RETRIES,
367 http_client_retry_initial_delay_ms: DEFAULT_HTTP_CLIENT_RETRY_INITIAL_DELAY_MS,
368 http_client_retry_max_delay_ms: DEFAULT_HTTP_CLIENT_RETRY_MAX_DELAY_MS,
369 cache_max_size: DEFAULT_CACHE_MAX_SIZE,
370 cache_default_ttl_secs: DEFAULT_CACHE_DEFAULT_TTL_SECS,
371 rate_limit_per_second: DEFAULT_RATE_LIMIT_PER_SECOND,
372 concurrent_request_limit: DEFAULT_CONCURRENT_REQUEST_LIMIT,
373 enable_response_compression: true,
374 enable_metrics: true,
375 metrics_port: 0,
376 }
377 }
378}
379
380/// Environment variable configuration for server
381///
382/// All fields are `Option<T>` to distinguish between "not set from environment"
383/// and "explicitly set from environment".
384///
385/// # Semantics
386///
387/// - `None` - The environment variable was not set; use the config file or default value
388/// - `Some(value)` - The environment variable was explicitly set to `value`
389///
390/// # Example
391///
392/// ```rust,ignore
393/// // CRATES_DOCS_HOST not set
394/// let config = EnvServerConfig::from_env(); // host == None, use default
395///
396/// // CRATES_DOCS_HOST=127.0.0.1
397/// let config = EnvServerConfig::from_env(); // host == Some("127.0.0.1")
398/// ```
399#[derive(Debug, Clone, Default)]
400pub struct EnvServerConfig {
401 /// Server name
402 pub name: Option<String>,
403 /// Host address
404 pub host: Option<String>,
405 /// Port
406 pub port: Option<u16>,
407 /// Transport mode
408 pub transport_mode: Option<String>,
409}
410
411/// Environment variable configuration for logging
412///
413/// All fields are `Option<T>` to distinguish between "not set from environment"
414/// and "explicitly set from environment".
415///
416/// # Semantics
417///
418/// - `None` - The environment variable was not set; use the config file or default value
419/// - `Some(value)` - The environment variable was explicitly set to `value`
420#[derive(Debug, Clone, Default)]
421pub struct EnvLoggingConfig {
422 /// Log level
423 pub level: Option<String>,
424 /// Whether to enable console logging
425 pub enable_console: Option<bool>,
426 /// Whether to enable file logging
427 pub enable_file: Option<bool>,
428}
429
430/// Environment variable configuration for API key (when feature enabled)
431///
432/// All fields are `Option<T>` to distinguish between "not set from environment"
433/// and "explicitly set from environment".
434///
435/// # Semantics
436///
437/// - `None` - The environment variable was not set; use the config file or default value
438/// - `Some(value)` - The environment variable was explicitly set to `value`
439#[cfg(feature = "api-key")]
440#[derive(Debug, Clone, Default)]
441pub struct EnvApiKeyConfig {
442 /// Whether API key authentication is enabled
443 pub enabled: Option<bool>,
444 /// List of valid API keys
445 pub keys: Option<Vec<String>>,
446 /// Header name for API key
447 pub header_name: Option<String>,
448 /// Query parameter name for API key
449 pub query_param_name: Option<String>,
450 /// Whether to allow API key in query parameters
451 pub allow_query_param: Option<bool>,
452 /// API key prefix
453 pub key_prefix: Option<String>,
454}
455
456/// Environment variable configuration
457///
458/// Uses `Option<T>` for all fields to properly distinguish between
459/// "not set" and "explicitly set to default value".
460#[derive(Debug, Clone, Default)]
461pub struct EnvAppConfig {
462 /// Server configuration from environment
463 pub server: EnvServerConfig,
464 /// Logging configuration from environment
465 pub logging: EnvLoggingConfig,
466 /// API key configuration from environment
467 #[cfg(feature = "api-key")]
468 pub auth_api_key: EnvApiKeyConfig,
469}
470
471impl AppConfig {
472 /// Load configuration from file
473 ///
474 /// # Errors
475 ///
476 /// Returns an error if file does not exist, cannot be read, or format is invalid
477 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
478 let content = fs::read_to_string(path).map_err(|e| {
479 crate::error::Error::config("file", format!("Failed to read config file: {e}"))
480 })?;
481
482 let config: Self = toml::from_str(&content).map_err(|e| {
483 crate::error::Error::parse("config", None, format!("Failed to parse config file: {e}"))
484 })?;
485
486 config.validate()?;
487 Ok(config)
488 }
489
490 /// Save configuration to file
491 ///
492 /// # Errors
493 ///
494 /// Returns an error if configuration cannot be serialized, directory cannot be created, or file cannot be written
495 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
496 let content = toml::to_string_pretty(self).map_err(|e| {
497 crate::error::Error::config(
498 "serialization",
499 format!("Failed to serialize configuration: {e}"),
500 )
501 })?;
502
503 // Ensure directory exists
504 if let Some(parent) = path.as_ref().parent() {
505 fs::create_dir_all(parent).map_err(|e| {
506 crate::error::Error::config("directory", format!("Failed to create directory: {e}"))
507 })?;
508 }
509
510 fs::write(path, content).map_err(|e| {
511 crate::error::Error::config("file", format!("Failed to write config file: {e}"))
512 })?;
513
514 Ok(())
515 }
516
517 /// Validate configuration
518 ///
519 /// # Errors
520 ///
521 /// Returns an error if configuration is invalid (e.g., empty hostname, invalid port, etc.)
522 pub fn validate(&self) -> Result<(), crate::error::Error> {
523 // Validate server configuration
524 if self.server.host.is_empty() {
525 return Err(crate::error::Error::config("host", "cannot be empty"));
526 }
527
528 if self.server.port == 0 {
529 return Err(crate::error::Error::config("port", "cannot be 0"));
530 }
531
532 if self.server.max_connections == 0 {
533 return Err(crate::error::Error::config(
534 "max_connections",
535 "cannot be 0",
536 ));
537 }
538
539 // Validate transport mode
540 let valid_modes = ["stdio", "http", "sse", "hybrid"];
541 if !valid_modes.contains(&self.server.transport_mode.as_str()) {
542 return Err(crate::error::Error::config(
543 "transport_mode",
544 format!(
545 "Invalid transport mode: {}, valid values: {:?}",
546 self.server.transport_mode, valid_modes
547 ),
548 ));
549 }
550
551 // Validate log level
552 let valid_levels = ["trace", "debug", "info", "warn", "error"];
553
554 if !valid_levels.contains(&self.logging.level.as_str()) {
555 return Err(crate::error::Error::config(
556 "log_level",
557 format!(
558 "Invalid log level: {}, valid values: {:?}",
559 self.logging.level, valid_levels
560 ),
561 ));
562 }
563
564 // Validate performance configuration
565 if self.performance.http_client_pool_size == 0 {
566 return Err(crate::error::Error::config(
567 "http_client_pool_size",
568 "cannot be 0",
569 ));
570 }
571
572 if self.performance.http_client_pool_idle_timeout_secs == 0 {
573 return Err(crate::error::Error::config(
574 "http_client_pool_idle_timeout_secs",
575 "cannot be 0",
576 ));
577 }
578
579 if self.performance.http_client_connect_timeout_secs == 0 {
580 return Err(crate::error::Error::config(
581 "http_client_connect_timeout_secs",
582 "cannot be 0",
583 ));
584 }
585
586 if self.performance.http_client_timeout_secs == 0 {
587 return Err(crate::error::Error::config(
588 "http_client_timeout_secs",
589 "cannot be 0",
590 ));
591 }
592
593 if self.performance.cache_max_size == 0 {
594 return Err(crate::error::Error::config("cache_max_size", "cannot be 0"));
595 }
596
597 // Validate OAuth configuration
598 if self.server.enable_oauth {
599 self.oauth.validate()?;
600 }
601
602 Ok(())
603 }
604
605 /// Load configuration from environment variables
606 ///
607 /// Returns an `EnvAppConfig` where all fields are `Option<T>`, allowing
608 /// the caller to distinguish between "not set" and "explicitly set".
609 ///
610 /// # Errors
611 ///
612 /// Returns an error if environment variable format is invalid (e.g., non-numeric port)
613 pub fn from_env() -> Result<EnvAppConfig, crate::error::Error> {
614 let mut config = EnvAppConfig::default();
615
616 // Load server configuration from environment variables
617 if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
618 config.server.name = Some(name);
619 }
620
621 if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
622 config.server.host = Some(host);
623 }
624
625 if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
626 config.server.port =
627 Some(port.parse().map_err(|e| {
628 crate::error::Error::config("port", format!("Invalid port: {e}"))
629 })?);
630 }
631
632 if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
633 config.server.transport_mode = Some(mode);
634 }
635
636 // Load logging configuration from environment variables
637 if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
638 config.logging.level = Some(level);
639 }
640
641 if let Ok(enable_console) = std::env::var("CRATES_DOCS_ENABLE_CONSOLE") {
642 config.logging.enable_console = enable_console.parse().ok();
643 }
644
645 if let Ok(enable_file) = std::env::var("CRATES_DOCS_ENABLE_FILE") {
646 config.logging.enable_file = enable_file.parse().ok();
647 }
648
649 #[cfg(feature = "api-key")]
650 {
651 if let Ok(enabled) = std::env::var("CRATES_DOCS_API_KEY_ENABLED") {
652 config.auth_api_key.enabled = enabled.parse().ok();
653 }
654
655 if let Ok(keys) = std::env::var("CRATES_DOCS_API_KEYS") {
656 config.auth_api_key.keys = Some(
657 keys.split(',')
658 .map(str::trim)
659 .filter(|s| !s.is_empty())
660 .map(ToOwned::to_owned)
661 .collect(),
662 );
663 }
664
665 if let Ok(header_name) = std::env::var("CRATES_DOCS_API_KEY_HEADER") {
666 config.auth_api_key.header_name = Some(header_name);
667 }
668
669 if let Ok(query_param_name) = std::env::var("CRATES_DOCS_API_KEY_QUERY_PARAM_NAME") {
670 config.auth_api_key.query_param_name = Some(query_param_name);
671 }
672
673 if let Ok(allow_query_param) = std::env::var("CRATES_DOCS_API_KEY_ALLOW_QUERY") {
674 config.auth_api_key.allow_query_param = allow_query_param.parse().ok();
675 }
676
677 if let Ok(key_prefix) = std::env::var("CRATES_DOCS_API_KEY_PREFIX") {
678 config.auth_api_key.key_prefix = Some(key_prefix);
679 }
680 }
681
682 Ok(config)
683 }
684
685 /// Merge configuration (environment variables take precedence over file configuration)
686 ///
687 /// Uses `Option<T>` semantics from `EnvAppConfig` to determine which values
688 /// were explicitly set via environment variables. This eliminates fragile
689 /// hardcoded default comparisons.
690 #[must_use]
691 pub fn merge(file_config: Option<Self>, env_config: Option<EnvAppConfig>) -> Self {
692 let mut config = Self::default();
693
694 // First apply file configuration
695 if let Some(file) = file_config {
696 config = file;
697 }
698
699 // Then apply environment variable configuration (overrides file configuration)
700 // Uses Option::is_some() to check if value was explicitly set
701 if let Some(env) = env_config {
702 // Merge server configuration - only override if explicitly set
703 if let Some(name) = env.server.name {
704 config.server.name = name;
705 }
706 if let Some(host) = env.server.host {
707 config.server.host = host;
708 }
709 if let Some(port) = env.server.port {
710 config.server.port = port;
711 }
712 if let Some(transport_mode) = env.server.transport_mode {
713 config.server.transport_mode = transport_mode;
714 }
715
716 // Merge logging configuration - only override if explicitly set
717 if let Some(level) = env.logging.level {
718 config.logging.level = level;
719 }
720 if let Some(enable_console) = env.logging.enable_console {
721 config.logging.enable_console = enable_console;
722 }
723 if let Some(enable_file) = env.logging.enable_file {
724 config.logging.enable_file = enable_file;
725 }
726
727 #[cfg(feature = "api-key")]
728 {
729 if let Some(enabled) = env.auth_api_key.enabled {
730 config.auth.api_key.enabled = enabled;
731 }
732 if let Some(keys) = env.auth_api_key.keys {
733 config.auth.api_key.keys = keys;
734 }
735 if let Some(header_name) = env.auth_api_key.header_name {
736 config.auth.api_key.header_name = header_name;
737 }
738 if let Some(query_param_name) = env.auth_api_key.query_param_name {
739 config.auth.api_key.query_param_name = query_param_name;
740 }
741 if let Some(allow_query_param) = env.auth_api_key.allow_query_param {
742 config.auth.api_key.allow_query_param = allow_query_param;
743 }
744 if let Some(key_prefix) = env.auth_api_key.key_prefix {
745 config.auth.api_key.key_prefix = key_prefix;
746 }
747 }
748 }
749
750 config
751 }
752}