1use crate::cache::CacheConfig;
4use crate::server::auth::OAuthConfig;
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::Path;
8
9#[derive(Debug, Clone, Deserialize, Serialize, Default)]
11pub struct AppConfig {
12 pub server: ServerConfig,
14
15 pub cache: CacheConfig,
17
18 pub oauth: OAuthConfig,
20
21 pub logging: LoggingConfig,
23
24 pub performance: PerformanceConfig,
26}
27
28#[derive(Debug, Clone, Deserialize, Serialize)]
30pub struct ServerConfig {
31 pub name: String,
33
34 pub version: String,
36
37 pub description: Option<String>,
39
40 pub host: String,
42
43 pub port: u16,
45
46 pub transport_mode: String,
48
49 pub enable_sse: bool,
51
52 pub enable_oauth: bool,
54
55 pub max_connections: usize,
57
58 pub request_timeout_secs: u64,
60
61 pub response_timeout_secs: u64,
63
64 pub allowed_hosts: Vec<String>,
66
67 pub allowed_origins: Vec<String>,
70}
71
72#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct LoggingConfig {
75 pub level: String,
77
78 pub file_path: Option<String>,
80
81 pub enable_console: bool,
83
84 pub enable_file: bool,
86
87 pub max_file_size_mb: u64,
89
90 pub max_files: usize,
92}
93
94#[derive(Debug, Clone, Deserialize, Serialize)]
96pub struct PerformanceConfig {
97 pub http_client_pool_size: usize,
99
100 pub cache_max_size: usize,
102
103 pub cache_default_ttl_secs: u64,
105
106 pub rate_limit_per_second: u32,
108
109 pub concurrent_request_limit: usize,
111
112 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 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 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 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 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 pub fn validate(&self) -> Result<(), crate::error::Error> {
213 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 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 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 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 if self.server.enable_oauth {
265 self.oauth.validate()?;
266 }
267
268 Ok(())
269 }
270
271 pub fn from_env() -> Result<Self, crate::error::Error> {
277 let mut config = Self::default();
278
279 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 #[must_use]
308 pub fn merge(file_config: Option<Self>, env_config: Option<Self>) -> Self {
309 let mut config = Self::default();
310
311 if let Some(file) = file_config {
313 config = file;
314 }
315
316 if let Some(env) = env_config {
318 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 if env.logging.level != "info" {
334 config.logging.level = env.logging.level;
335 }
336 }
337
338 config
339 }
340}