Skip to main content

crates_docs/config/
mod.rs

1//! Configuration module
2
3use crate::cache::CacheConfig;
4use crate::server::auth::OAuthConfig;
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::Path;
8
9/// Application configuration
10#[derive(Debug, Clone, Deserialize, Serialize, Default)]
11pub struct AppConfig {
12    /// Server configuration
13    pub server: ServerConfig,
14
15    /// Cache configuration
16    pub cache: CacheConfig,
17
18    /// OAuth configuration
19    pub oauth: OAuthConfig,
20
21    /// Logging configuration
22    pub logging: LoggingConfig,
23
24    /// Performance configuration
25    pub performance: PerformanceConfig,
26}
27
28/// Server configuration
29#[derive(Debug, Clone, Deserialize, Serialize)]
30pub struct ServerConfig {
31    /// Server name
32    pub name: String,
33
34    /// Server version
35    pub version: String,
36
37    /// Server description
38    pub description: Option<String>,
39
40    /// Host address
41    pub host: String,
42
43    /// Port
44    pub port: u16,
45
46    /// Transport mode
47    pub transport_mode: String,
48
49    /// Enable SSE support
50    pub enable_sse: bool,
51
52    /// Enable OAuth authentication
53    pub enable_oauth: bool,
54
55    /// Maximum concurrent connections
56    pub max_connections: usize,
57
58    /// Request timeout (seconds)
59    pub request_timeout_secs: u64,
60
61    /// Response timeout (seconds)
62    pub response_timeout_secs: u64,
63
64    /// Allowed hosts for CORS (e.g., `["localhost", "127.0.0.1"]`)
65    pub allowed_hosts: Vec<String>,
66
67    /// Allowed origins for CORS (e.g., `["http://localhost:*"]`)
68    /// Use `"*"` only in development, specify exact origins in production
69    pub allowed_origins: Vec<String>,
70}
71
72/// Logging configuration
73#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct LoggingConfig {
75    /// Log level
76    pub level: String,
77
78    /// Log file path
79    pub file_path: Option<String>,
80
81    /// Whether to enable console logging
82    pub enable_console: bool,
83
84    /// Whether to enable file logging
85    pub enable_file: bool,
86
87    /// Maximum log file size (MB)
88    pub max_file_size_mb: u64,
89
90    /// Number of log files to retain
91    pub max_files: usize,
92}
93
94/// Performance configuration
95#[derive(Debug, Clone, Deserialize, Serialize)]
96pub struct PerformanceConfig {
97    /// HTTP client connection pool size
98    pub http_client_pool_size: usize,
99
100    /// Maximum cache size (number of entries)
101    pub cache_max_size: usize,
102
103    /// Default cache TTL (seconds)
104    pub cache_default_ttl_secs: u64,
105
106    /// Request rate limit (requests per second)
107    pub rate_limit_per_second: u32,
108
109    /// Concurrent request limit
110    pub concurrent_request_limit: usize,
111
112    /// Enable response compression
113    pub enable_response_compression: bool,
114}
115
116impl Default for ServerConfig {
117    fn default() -> Self {
118        Self {
119            name: "crates-docs".to_string(),
120            version: crate::VERSION.to_string(),
121            description: Some(
122                "High-performance Rust crate documentation query MCP server".to_string(),
123            ),
124            host: "127.0.0.1".to_string(),
125            port: 8080,
126            transport_mode: "hybrid".to_string(),
127            enable_sse: true,
128            enable_oauth: false,
129            max_connections: 100,
130            request_timeout_secs: 30,
131            response_timeout_secs: 60,
132            // Secure defaults: only allow localhost by default
133            allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
134            allowed_origins: vec!["http://localhost:*".to_string()],
135        }
136    }
137}
138
139impl Default for LoggingConfig {
140    fn default() -> Self {
141        Self {
142            level: "info".to_string(),
143            file_path: Some("./logs/crates-docs.log".to_string()),
144            enable_console: true,
145            enable_file: true,
146            max_file_size_mb: 100,
147            max_files: 10,
148        }
149    }
150}
151
152impl Default for PerformanceConfig {
153    fn default() -> Self {
154        Self {
155            http_client_pool_size: 10,
156            cache_max_size: 1000,
157            cache_default_ttl_secs: 3600,
158            rate_limit_per_second: 100,
159            concurrent_request_limit: 50,
160            enable_response_compression: true,
161        }
162    }
163}
164
165impl AppConfig {
166    /// Load configuration from file
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if file does not exist, cannot be read, or format is invalid
171    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
172        let content = fs::read_to_string(path)
173            .map_err(|e| crate::error::Error::Config(format!("Failed to read config file: {e}")))?;
174
175        let config: Self = toml::from_str(&content).map_err(|e| {
176            crate::error::Error::Config(format!("Failed to parse config file: {e}"))
177        })?;
178
179        config.validate()?;
180        Ok(config)
181    }
182
183    /// Save configuration to file
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if configuration cannot be serialized, directory cannot be created, or file cannot be written
188    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
189        let content = toml::to_string_pretty(self).map_err(|e| {
190            crate::error::Error::Config(format!("Failed to serialize configuration: {e}"))
191        })?;
192
193        // Ensure directory exists
194        if let Some(parent) = path.as_ref().parent() {
195            fs::create_dir_all(parent).map_err(|e| {
196                crate::error::Error::Config(format!("Failed to create directory: {e}"))
197            })?;
198        }
199
200        fs::write(path, content).map_err(|e| {
201            crate::error::Error::Config(format!("Failed to write config file: {e}"))
202        })?;
203
204        Ok(())
205    }
206
207    /// Validate configuration
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if configuration is invalid (e.g., empty hostname, invalid port, etc.)
212    pub fn validate(&self) -> Result<(), crate::error::Error> {
213        // Validate server configuration
214        if self.server.host.is_empty() {
215            return Err(crate::error::Error::Config(
216                "Server host cannot be empty".to_string(),
217            ));
218        }
219
220        if self.server.port == 0 {
221            return Err(crate::error::Error::Config(
222                "Server port cannot be 0".to_string(),
223            ));
224        }
225
226        if self.server.max_connections == 0 {
227            return Err(crate::error::Error::Config(
228                "Maximum connections cannot be 0".to_string(),
229            ));
230        }
231
232        // Validate transport mode
233        let valid_modes = ["stdio", "http", "sse", "hybrid"];
234        if !valid_modes.contains(&self.server.transport_mode.as_str()) {
235            return Err(crate::error::Error::Config(format!(
236                "Invalid transport mode: {}, valid values: {:?}",
237                self.server.transport_mode, valid_modes
238            )));
239        }
240
241        // Validate log level
242        let valid_levels = ["trace", "debug", "info", "warn", "error"];
243        if !valid_levels.contains(&self.logging.level.as_str()) {
244            return Err(crate::error::Error::Config(format!(
245                "Invalid log level: {}, valid values: {:?}",
246                self.logging.level, valid_levels
247            )));
248        }
249
250        // Validate performance configuration
251        if self.performance.http_client_pool_size == 0 {
252            return Err(crate::error::Error::Config(
253                "HTTP client connection pool size cannot be 0".to_string(),
254            ));
255        }
256
257        if self.performance.cache_max_size == 0 {
258            return Err(crate::error::Error::Config(
259                "Maximum cache size cannot be 0".to_string(),
260            ));
261        }
262
263        // Validate OAuth configuration
264        if self.server.enable_oauth {
265            self.oauth.validate()?;
266        }
267
268        Ok(())
269    }
270
271    /// Load configuration from environment variables
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if environment variable format is invalid or configuration validation fails
276    pub fn from_env() -> Result<Self, crate::error::Error> {
277        let mut config = Self::default();
278
279        // Override configuration from environment variables
280        if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
281            config.server.name = name;
282        }
283
284        if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
285            config.server.host = host;
286        }
287
288        if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
289            config.server.port = port
290                .parse()
291                .map_err(|e| crate::error::Error::Config(format!("Invalid port: {e}")))?;
292        }
293
294        if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
295            config.server.transport_mode = mode;
296        }
297
298        if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
299            config.logging.level = level;
300        }
301
302        config.validate()?;
303        Ok(config)
304    }
305
306    /// Merge configuration (environment variables take precedence over file configuration)
307    #[must_use]
308    pub fn merge(file_config: Option<Self>, env_config: Option<Self>) -> Self {
309        let mut config = Self::default();
310
311        // First apply file configuration
312        if let Some(file) = file_config {
313            config = file;
314        }
315
316        // Then apply environment variable configuration (overrides file configuration)
317        if let Some(env) = env_config {
318            // Merge server configuration
319            if env.server.name != "crates-docs" {
320                config.server.name = env.server.name;
321            }
322            if env.server.host != "127.0.0.1" {
323                config.server.host = env.server.host;
324            }
325            if env.server.port != 8080 {
326                config.server.port = env.server.port;
327            }
328            if env.server.transport_mode != "hybrid" {
329                config.server.transport_mode = env.server.transport_mode;
330            }
331
332            // Merge logging configuration
333            if env.logging.level != "info" {
334                config.logging.level = env.logging.level;
335            }
336        }
337
338        config
339    }
340}