Skip to main content

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}