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