Skip to main content

crates_docs/config/
mod.rs

1//! 配置模块
2//!
3//! 提供应用程序配置管理,支持从文件加载、环境变量和默认值。
4//!
5//! # 配置来源优先级
6//!
7//! 1. 环境变量(最高优先级)
8//! 2. 配置文件
9//! 3. 默认值(最低优先级)
10//!
11//! # 支持的配置格式
12//!
13//! - TOML 配置文件
14//! - 环境变量(前缀 `CRATES_DOCS_`)
15//!
16//! # 示例
17//!
18//! ```rust,no_run
19//! use crates_docs::config::AppConfig;
20//!
21//! // 从文件加载配置
22//! let config = AppConfig::from_file("config.toml").expect("Failed to load config");
23//!
24//! // 从环境变量加载配置
25//! let config = AppConfig::from_env().expect("Failed to load config from env");
26//!
27//! // 使用默认配置
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/// 应用程序配置
39///
40/// 包含服务器、缓存、认证、日志和性能配置。
41///
42/// # 字段
43///
44/// - `server`: 服务器配置
45/// - `cache`: 缓存配置
46/// - `auth`: 认证配置(OAuth 和 API Key)
47/// - `logging`: 日志配置
48/// - `performance`: 性能配置
49#[derive(Debug, Clone, Deserialize, Serialize, Default)]
50pub struct AppConfig {
51    /// 服务器配置
52    pub server: ServerConfig,
53
54    /// 缓存配置
55    pub cache: CacheConfig,
56
57    /// 认证配置(OAuth 和 API Key)
58    #[serde(default)]
59    pub auth: AuthConfig,
60
61    /// OAuth 配置(向后兼容,优先使用 auth.oauth)
62    #[serde(default)]
63    pub oauth: OAuthConfig,
64
65    /// 日志配置
66    pub logging: LoggingConfig,
67
68    /// 性能配置
69    pub performance: PerformanceConfig,
70}
71
72/// Server configuration
73#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct ServerConfig {
75    /// Server name
76    pub name: String,
77
78    /// Server version
79    #[serde(default = "default_version")]
80    pub version: String,
81
82    /// Server description
83    pub description: Option<String>,
84
85    /// Server icons
86    #[serde(default = "default_icons")]
87    pub icons: Vec<Icon>,
88
89    /// Website URL
90    pub website_url: Option<String>,
91
92    /// Host address
93    pub host: String,
94
95    /// Port
96    pub port: u16,
97
98    /// Transport mode
99    pub transport_mode: String,
100
101    /// Enable SSE support
102    pub enable_sse: bool,
103
104    /// Enable OAuth authentication
105    pub enable_oauth: bool,
106
107    /// Maximum concurrent connections
108    pub max_connections: usize,
109
110    /// Request timeout (seconds)
111    pub request_timeout_secs: u64,
112
113    /// Response timeout (seconds)
114    pub response_timeout_secs: u64,
115
116    /// Allowed hosts for CORS (e.g., `["localhost", "127.0.0.1"]`)
117    pub allowed_hosts: Vec<String>,
118
119    /// Allowed origins for CORS (e.g., `["http://localhost:*"]`)
120    /// Use `"*"` only in development, specify exact origins in production
121    pub allowed_origins: Vec<String>,
122}
123
124/// Default server version from Cargo.toml
125fn default_version() -> String {
126    crate::VERSION.to_string()
127}
128
129/// Default icons for the server
130fn default_icons() -> Vec<Icon> {
131    vec![
132        Icon {
133            src: "https://docs.rs/static/favicon-32x32.png".to_string(),
134            mime_type: Some("image/png".to_string()),
135            sizes: vec!["32x32".to_string()],
136            theme: Some(IconTheme::Light),
137        },
138        Icon {
139            src: "https://docs.rs/static/favicon-32x32.png".to_string(),
140            mime_type: Some("image/png".to_string()),
141            sizes: vec!["32x32".to_string()],
142            theme: Some(IconTheme::Dark),
143        },
144    ]
145}
146
147/// Logging configuration
148#[derive(Debug, Clone, Deserialize, Serialize)]
149pub struct LoggingConfig {
150    /// Log level
151    pub level: String,
152
153    /// Log file path
154    pub file_path: Option<String>,
155
156    /// Whether to enable console logging
157    pub enable_console: bool,
158
159    /// Whether to enable file logging
160    pub enable_file: bool,
161
162    /// Maximum log file size (MB)
163    pub max_file_size_mb: u64,
164
165    /// Number of log files to retain
166    pub max_files: usize,
167}
168
169/// Performance configuration
170#[derive(Debug, Clone, Deserialize, Serialize)]
171pub struct PerformanceConfig {
172    /// HTTP client connection pool size
173    pub http_client_pool_size: usize,
174
175    /// HTTP client pool idle timeout (seconds)
176    pub http_client_pool_idle_timeout_secs: u64,
177
178    /// HTTP client connection timeout (seconds)
179    pub http_client_connect_timeout_secs: u64,
180
181    /// HTTP client request timeout (seconds)
182    pub http_client_timeout_secs: u64,
183
184    /// HTTP client read timeout (seconds)
185    pub http_client_read_timeout_secs: u64,
186
187    /// HTTP client max retry attempts
188    pub http_client_max_retries: u32,
189
190    /// HTTP client retry initial delay (milliseconds)
191    pub http_client_retry_initial_delay_ms: u64,
192
193    /// HTTP client retry max delay (milliseconds)
194    pub http_client_retry_max_delay_ms: u64,
195
196    /// Maximum cache size (number of entries)
197    pub cache_max_size: usize,
198
199    /// Default cache TTL (seconds)
200    pub cache_default_ttl_secs: u64,
201
202    /// Request rate limit (requests per second)
203    pub rate_limit_per_second: u32,
204
205    /// Concurrent request limit
206    pub concurrent_request_limit: usize,
207
208    /// Enable response compression
209    pub enable_response_compression: bool,
210
211    /// Enable Prometheus metrics
212    pub enable_metrics: bool,
213
214    /// Metrics endpoint port (0 = use server port)
215    pub metrics_port: u16,
216}
217
218impl Default for ServerConfig {
219    fn default() -> Self {
220        Self {
221            name: "crates-docs".to_string(),
222            version: crate::VERSION.to_string(),
223            description: Some(
224                "High-performance Rust crate documentation query MCP server".to_string(),
225            ),
226            icons: default_icons(),
227            website_url: Some("https://github.com/KingingWang/crates-docs".to_string()),
228            host: "127.0.0.1".to_string(),
229            port: 8080,
230            transport_mode: "hybrid".to_string(),
231            enable_sse: true,
232            enable_oauth: false,
233            max_connections: 100,
234            request_timeout_secs: 30,
235            response_timeout_secs: 60,
236            // Secure defaults: only allow localhost by default
237            allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
238            allowed_origins: vec!["http://localhost:*".to_string()],
239        }
240    }
241}
242
243impl Default for LoggingConfig {
244    fn default() -> Self {
245        Self {
246            level: "info".to_string(),
247            file_path: Some("./logs/crates-docs.log".to_string()),
248            enable_console: true,
249            enable_file: false, // 默认仅输出到控制台
250            max_file_size_mb: 100,
251            max_files: 10,
252        }
253    }
254}
255
256impl Default for PerformanceConfig {
257    fn default() -> Self {
258        Self {
259            http_client_pool_size: 10,
260            http_client_pool_idle_timeout_secs: 90,
261            http_client_connect_timeout_secs: 10,
262            http_client_timeout_secs: 30,
263            http_client_read_timeout_secs: 30,
264            http_client_max_retries: 3,
265            http_client_retry_initial_delay_ms: 100,
266            http_client_retry_max_delay_ms: 10000,
267            cache_max_size: 1000,
268            cache_default_ttl_secs: 3600,
269            rate_limit_per_second: 100,
270            concurrent_request_limit: 50,
271            enable_response_compression: true,
272            enable_metrics: true,
273            metrics_port: 0,
274        }
275    }
276}
277
278impl AppConfig {
279    /// Load configuration from file
280    ///
281    /// # Errors
282    ///
283    /// Returns an error if file does not exist, cannot be read, or format is invalid
284    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
285        let content = fs::read_to_string(path).map_err(|e| {
286            crate::error::Error::config("file", format!("Failed to read config file: {e}"))
287        })?;
288
289        let config: Self = toml::from_str(&content).map_err(|e| {
290            crate::error::Error::parse("config", None, format!("Failed to parse config file: {e}"))
291        })?;
292
293        config.validate()?;
294        Ok(config)
295    }
296
297    /// Save configuration to file
298    ///
299    /// # Errors
300    ///
301    /// Returns an error if configuration cannot be serialized, directory cannot be created, or file cannot be written
302    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
303        let content = toml::to_string_pretty(self).map_err(|e| {
304            crate::error::Error::config(
305                "serialization",
306                format!("Failed to serialize configuration: {e}"),
307            )
308        })?;
309
310        // Ensure directory exists
311        if let Some(parent) = path.as_ref().parent() {
312            fs::create_dir_all(parent).map_err(|e| {
313                crate::error::Error::config("directory", format!("Failed to create directory: {e}"))
314            })?;
315        }
316
317        fs::write(path, content).map_err(|e| {
318            crate::error::Error::config("file", format!("Failed to write config file: {e}"))
319        })?;
320
321        Ok(())
322    }
323
324    /// Validate configuration
325    ///
326    /// # Errors
327    ///
328    /// Returns an error if configuration is invalid (e.g., empty hostname, invalid port, etc.)
329    pub fn validate(&self) -> Result<(), crate::error::Error> {
330        // Validate server configuration
331        if self.server.host.is_empty() {
332            return Err(crate::error::Error::config("host", "cannot be empty"));
333        }
334
335        if self.server.port == 0 {
336            return Err(crate::error::Error::config("port", "cannot be 0"));
337        }
338
339        if self.server.max_connections == 0 {
340            return Err(crate::error::Error::config(
341                "max_connections",
342                "cannot be 0",
343            ));
344        }
345
346        // Validate transport mode
347        let valid_modes = ["stdio", "http", "sse", "hybrid"];
348        if !valid_modes.contains(&self.server.transport_mode.as_str()) {
349            return Err(crate::error::Error::config(
350                "transport_mode",
351                format!(
352                    "Invalid transport mode: {}, valid values: {:?}",
353                    self.server.transport_mode, valid_modes
354                ),
355            ));
356        }
357
358        // Validate log level
359        let valid_levels = ["trace", "debug", "info", "warn", "error"];
360
361        if !valid_levels.contains(&self.logging.level.as_str()) {
362            return Err(crate::error::Error::config(
363                "log_level",
364                format!(
365                    "Invalid log level: {}, valid values: {:?}",
366                    self.logging.level, valid_levels
367                ),
368            ));
369        }
370
371        // Validate performance configuration
372        if self.performance.http_client_pool_size == 0 {
373            return Err(crate::error::Error::config(
374                "http_client_pool_size",
375                "cannot be 0",
376            ));
377        }
378
379        if self.performance.http_client_pool_idle_timeout_secs == 0 {
380            return Err(crate::error::Error::config(
381                "http_client_pool_idle_timeout_secs",
382                "cannot be 0",
383            ));
384        }
385
386        if self.performance.http_client_connect_timeout_secs == 0 {
387            return Err(crate::error::Error::config(
388                "http_client_connect_timeout_secs",
389                "cannot be 0",
390            ));
391        }
392
393        if self.performance.http_client_timeout_secs == 0 {
394            return Err(crate::error::Error::config(
395                "http_client_timeout_secs",
396                "cannot be 0",
397            ));
398        }
399
400        if self.performance.cache_max_size == 0 {
401            return Err(crate::error::Error::config("cache_max_size", "cannot be 0"));
402        }
403
404        // Validate OAuth configuration
405        if self.server.enable_oauth {
406            self.oauth.validate()?;
407        }
408
409        Ok(())
410    }
411
412    /// Load configuration from environment variables
413    ///
414    /// # Errors
415    ///
416    /// Returns an error if environment variable format is invalid or configuration validation fails
417    pub fn from_env() -> Result<Self, crate::error::Error> {
418        let mut config = Self::default();
419
420        // Override configuration from environment variables
421        if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
422            config.server.name = name;
423        }
424
425        if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
426            config.server.host = host;
427        }
428
429        if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
430            config.server.port = port
431                .parse()
432                .map_err(|e| crate::error::Error::config("port", format!("Invalid port: {e}")))?;
433        }
434
435        if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
436            config.server.transport_mode = mode;
437        }
438
439        if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
440            config.logging.level = level;
441        }
442
443        if let Ok(enable_console) = std::env::var("CRATES_DOCS_ENABLE_CONSOLE") {
444            config.logging.enable_console = enable_console.parse().unwrap_or(true);
445        }
446
447        if let Ok(enable_file) = std::env::var("CRATES_DOCS_ENABLE_FILE") {
448            config.logging.enable_file = enable_file.parse().unwrap_or(true);
449        }
450
451        #[cfg(feature = "api-key")]
452        {
453            if let Ok(enabled) = std::env::var("CRATES_DOCS_API_KEY_ENABLED") {
454                config.auth.api_key.enabled = enabled.parse().unwrap_or(false);
455            }
456
457            if let Ok(keys) = std::env::var("CRATES_DOCS_API_KEYS") {
458                config.auth.api_key.keys = keys
459                    .split(',')
460                    .map(str::trim)
461                    .filter(|s| !s.is_empty())
462                    .map(ToOwned::to_owned)
463                    .collect();
464            }
465
466            if let Ok(header_name) = std::env::var("CRATES_DOCS_API_KEY_HEADER") {
467                config.auth.api_key.header_name = header_name;
468            }
469
470            if let Ok(query_param_name) = std::env::var("CRATES_DOCS_API_KEY_QUERY_PARAM_NAME") {
471                config.auth.api_key.query_param_name = query_param_name;
472            }
473
474            if let Ok(allow_query_param) = std::env::var("CRATES_DOCS_API_KEY_ALLOW_QUERY") {
475                config.auth.api_key.allow_query_param = allow_query_param.parse().unwrap_or(false);
476            }
477
478            if let Ok(key_prefix) = std::env::var("CRATES_DOCS_API_KEY_PREFIX") {
479                config.auth.api_key.key_prefix = key_prefix;
480            }
481        }
482
483        config.validate()?;
484        Ok(config)
485    }
486
487    /// Merge configuration (environment variables take precedence over file configuration)
488    #[must_use]
489    pub fn merge(file_config: Option<Self>, env_config: Option<Self>) -> Self {
490        let mut config = Self::default();
491
492        // First apply file configuration
493        if let Some(file) = file_config {
494            config = file;
495        }
496
497        // Then apply environment variable configuration (overrides file configuration)
498        if let Some(env) = env_config {
499            // Merge server configuration
500            if env.server.name != "crates-docs" {
501                config.server.name = env.server.name;
502            }
503            if env.server.host != "127.0.0.1" {
504                config.server.host = env.server.host;
505            }
506            if env.server.port != 8080 {
507                config.server.port = env.server.port;
508            }
509            if env.server.transport_mode != "hybrid" {
510                config.server.transport_mode = env.server.transport_mode;
511            }
512
513            // Merge logging configuration
514            if env.logging.level != "info" {
515                config.logging.level = env.logging.level;
516            }
517
518            #[cfg(feature = "api-key")]
519            {
520                let default_api_key = crate::server::auth::ApiKeyConfig::default();
521
522                if env.auth.api_key.enabled != default_api_key.enabled {
523                    config.auth.api_key.enabled = env.auth.api_key.enabled;
524                }
525
526                if env.auth.api_key.keys != default_api_key.keys {
527                    config.auth.api_key.keys = env.auth.api_key.keys;
528                }
529
530                if env.auth.api_key.header_name != default_api_key.header_name {
531                    config.auth.api_key.header_name = env.auth.api_key.header_name;
532                }
533
534                if env.auth.api_key.query_param_name != default_api_key.query_param_name {
535                    config.auth.api_key.query_param_name = env.auth.api_key.query_param_name;
536                }
537
538                if env.auth.api_key.allow_query_param != default_api_key.allow_query_param {
539                    config.auth.api_key.allow_query_param = env.auth.api_key.allow_query_param;
540                }
541
542                if env.auth.api_key.key_prefix != default_api_key.key_prefix {
543                    config.auth.api_key.key_prefix = env.auth.api_key.key_prefix;
544                }
545            }
546        }
547
548        config
549    }
550}