1use crate::error::{ClamberError, Result};
4use config::{Config, Environment, File, FileFormat};
5use serde::Deserialize;
6use std::collections::HashMap;
7use std::env;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ConfigFormat {
13 Yaml,
15 Toml,
17 Json,
19}
20
21impl ConfigFormat {
22 pub fn from_extension(path: &Path) -> Option<Self> {
24 match path.extension()?.to_str()? {
25 "yaml" | "yml" => Some(ConfigFormat::Yaml),
26 "toml" => Some(ConfigFormat::Toml),
27 "json" => Some(ConfigFormat::Json),
28 _ => None,
29 }
30 }
31
32 fn to_file_format(self) -> FileFormat {
34 match self {
35 ConfigFormat::Yaml => FileFormat::Yaml,
36 ConfigFormat::Toml => FileFormat::Toml,
37 ConfigFormat::Json => FileFormat::Json,
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct ConfigBuilder {
45 files: Vec<(PathBuf, Option<ConfigFormat>)>,
47 env_prefix: Option<String>,
49 env_separator: String,
51 ignore_missing: bool,
53 defaults: HashMap<String, config::Value>,
55}
56
57impl Default for ConfigBuilder {
58 fn default() -> Self {
59 Self {
60 files: Vec::new(),
61 env_prefix: None,
62 env_separator: "__".to_string(),
63 ignore_missing: false,
64 defaults: HashMap::new(),
65 }
66 }
67}
68
69impl ConfigBuilder {
70 pub fn new() -> Self {
72 Self::default()
73 }
74
75 pub fn add_file<P: AsRef<Path>>(mut self, path: P, format: Option<ConfigFormat>) -> Self {
81 self.files.push((path.as_ref().to_path_buf(), format));
82 self
83 }
84
85 pub fn add_yaml_file<P: AsRef<Path>>(self, path: P) -> Self {
87 self.add_file(path, Some(ConfigFormat::Yaml))
88 }
89
90 pub fn add_toml_file<P: AsRef<Path>>(self, path: P) -> Self {
92 self.add_file(path, Some(ConfigFormat::Toml))
93 }
94
95 pub fn add_json_file<P: AsRef<Path>>(self, path: P) -> Self {
97 self.add_file(path, Some(ConfigFormat::Json))
98 }
99
100 pub fn with_env_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
105 self.env_prefix = Some(prefix.into());
106 self
107 }
108
109 pub fn with_env_separator<S: Into<String>>(mut self, separator: S) -> Self {
114 self.env_separator = separator.into();
115 self
116 }
117
118 pub fn ignore_missing_files(mut self, ignore: bool) -> Self {
120 self.ignore_missing = ignore;
121 self
122 }
123
124 pub fn with_default<K, V>(mut self, key: K, value: V) -> Result<Self>
130 where
131 K: Into<String>,
132 V: Into<config::Value>,
133 {
134 self.defaults.insert(key.into(), value.into());
135 Ok(self)
136 }
137
138 pub fn build<T>(self) -> Result<T>
143 where
144 T: for<'de> Deserialize<'de>,
145 {
146 let mut config_builder = Config::builder();
147
148 for (key, value) in self.defaults {
150 config_builder = config_builder.set_default(&key, value).map_err(|e| {
151 ClamberError::ConfigLoadError {
152 details: format!("设置默认值失败: {}", e),
153 }
154 })?;
155 }
156
157 for (path, format) in self.files {
159 let format = format
160 .or_else(|| ConfigFormat::from_extension(&path))
161 .ok_or_else(|| ClamberError::ConfigLoadError {
162 details: format!("无法推断配置文件格式: {:?}", path),
163 })?;
164
165 let file_config = File::from(path.clone())
166 .format(format.to_file_format())
167 .required(!self.ignore_missing);
168
169 config_builder = config_builder.add_source(file_config);
170 }
171
172 if let Some(prefix) = self.env_prefix {
174 let env_config = Environment::with_prefix(&prefix)
175 .separator(&self.env_separator)
176 .try_parsing(true)
177 .ignore_empty(true);
178 config_builder = config_builder.add_source(env_config);
179 }
180
181 let config = config_builder
183 .build()
184 .map_err(|e| ClamberError::ConfigLoadError {
185 details: e.to_string(),
186 })?;
187
188 config
190 .try_deserialize::<T>()
191 .map_err(|e| ClamberError::ConfigParseError {
192 details: e.to_string(),
193 })
194 }
195
196 pub fn build_raw(self) -> Result<Config> {
198 let mut config_builder = Config::builder();
199
200 for (key, value) in self.defaults {
202 config_builder = config_builder.set_default(&key, value).map_err(|e| {
203 ClamberError::ConfigLoadError {
204 details: format!("设置默认值失败: {}", e),
205 }
206 })?;
207 }
208
209 for (path, format) in self.files {
211 let format = format
212 .or_else(|| ConfigFormat::from_extension(&path))
213 .ok_or_else(|| ClamberError::ConfigLoadError {
214 details: format!("无法推断配置文件格式: {:?}", path),
215 })?;
216
217 let file_config = File::from(path.clone())
218 .format(format.to_file_format())
219 .required(!self.ignore_missing);
220
221 config_builder = config_builder.add_source(file_config);
222 }
223
224 if let Some(prefix) = self.env_prefix {
226 let env_config = Environment::with_prefix(&prefix)
227 .separator(&self.env_separator)
228 .try_parsing(true)
229 .ignore_empty(true);
230 config_builder = config_builder.add_source(env_config);
231 }
232
233 config_builder
235 .build()
236 .map_err(|e| ClamberError::ConfigLoadError {
237 details: e.to_string(),
238 })
239 }
240}
241
242pub struct ConfigManager;
244
245impl ConfigManager {
246 pub fn load_from_file<T, P>(path: P) -> Result<T>
254 where
255 T: for<'de> Deserialize<'de>,
256 P: AsRef<Path>,
257 {
258 ConfigBuilder::new().add_file(path, None).build()
259 }
260
261 pub fn load_with_env<T, P, S>(config_path: P, env_prefix: S) -> Result<T>
270 where
271 T: for<'de> Deserialize<'de>,
272 P: AsRef<Path>,
273 S: Into<String>,
274 {
275 ConfigBuilder::new()
276 .add_file(config_path, None)
277 .with_env_prefix(env_prefix)
278 .build()
279 }
280
281 pub fn load_multiple<T, P, S>(config_paths: Vec<P>, env_prefix: Option<S>) -> Result<T>
290 where
291 T: for<'de> Deserialize<'de>,
292 P: AsRef<Path>,
293 S: Into<String>,
294 {
295 let mut builder = ConfigBuilder::new().ignore_missing_files(true);
296
297 for path in config_paths {
298 builder = builder.add_file(path, None);
299 }
300
301 if let Some(prefix) = env_prefix {
302 builder = builder.with_env_prefix(prefix);
303 }
304
305 builder.build()
306 }
307
308 pub fn builder() -> ConfigBuilder {
310 ConfigBuilder::new()
311 }
312}
313
314pub fn load_config<T, P>(path: P) -> Result<T>
316where
317 T: for<'de> Deserialize<'de>,
318 P: AsRef<Path>,
319{
320 ConfigManager::load_from_file(path)
321}
322
323pub fn load_config_with_env<T, P, S>(config_path: P, env_prefix: S) -> Result<T>
325where
326 T: for<'de> Deserialize<'de>,
327 P: AsRef<Path>,
328 S: Into<String>,
329{
330 ConfigManager::load_with_env(config_path, env_prefix)
331}
332
333pub fn get_config_paths(name: &str) -> Vec<PathBuf> {
335 let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
336
337 vec![
338 current_dir.join(format!("{}.yaml", name)),
339 current_dir.join(format!("{}.yml", name)),
340 current_dir.join(format!("{}.toml", name)),
341 current_dir.join(format!("{}.json", name)),
342 current_dir.join("config").join(format!("{}.yaml", name)),
343 current_dir.join("config").join(format!("{}.yml", name)),
344 current_dir.join("config").join(format!("{}.toml", name)),
345 current_dir.join("config").join(format!("{}.json", name)),
346 ]
347}
348
349pub fn auto_load_config<T>(name: &str, env_prefix: Option<&str>) -> Result<T>
351where
352 T: for<'de> Deserialize<'de>,
353{
354 let config_paths = get_config_paths(name);
355 ConfigManager::load_multiple(config_paths, env_prefix)
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use serde::{Deserialize, Serialize};
362 use std::fs;
363 use tempfile::tempdir;
364
365 #[derive(Debug, Serialize, Deserialize, PartialEq)]
366 struct TestConfig {
367 name: String,
368 port: u16,
369 debug: bool,
370 database: DatabaseConfig,
371 }
372
373 #[derive(Debug, Serialize, Deserialize, PartialEq)]
374 struct DatabaseConfig {
375 host: String,
376 port: u16,
377 username: String,
378 password: String,
379 }
380
381 impl Default for TestConfig {
382 fn default() -> Self {
383 Self {
384 name: "test-app".to_string(),
385 port: 8080,
386 debug: false,
387 database: DatabaseConfig {
388 host: "localhost".to_string(),
389 port: 5432,
390 username: "user".to_string(),
391 password: "password".to_string(),
392 },
393 }
394 }
395 }
396
397 #[test]
398 fn test_config_format_from_extension() {
399 assert_eq!(
400 ConfigFormat::from_extension(Path::new("config.yaml")),
401 Some(ConfigFormat::Yaml)
402 );
403 assert_eq!(
404 ConfigFormat::from_extension(Path::new("config.yml")),
405 Some(ConfigFormat::Yaml)
406 );
407 assert_eq!(
408 ConfigFormat::from_extension(Path::new("config.toml")),
409 Some(ConfigFormat::Toml)
410 );
411 assert_eq!(
412 ConfigFormat::from_extension(Path::new("config.json")),
413 Some(ConfigFormat::Json)
414 );
415 assert_eq!(ConfigFormat::from_extension(Path::new("config.txt")), None);
416 }
417
418 #[test]
419 fn test_load_yaml_config() {
420 let dir = tempdir().unwrap();
421 let config_path = dir.path().join("config.yaml");
422
423 let yaml_content = r#"
424name: "test-service"
425port: 3000
426debug: true
427database:
428 host: "db.example.com"
429 port: 5432
430 username: "testuser"
431 password: "testpass"
432"#;
433
434 fs::write(&config_path, yaml_content).unwrap();
435
436 let config: TestConfig = ConfigManager::load_from_file(&config_path).unwrap();
437
438 assert_eq!(config.name, "test-service");
439 assert_eq!(config.port, 3000);
440 assert_eq!(config.debug, true);
441 assert_eq!(config.database.host, "db.example.com");
442 }
443
444 #[test]
445 fn test_load_toml_config() {
446 let dir = tempdir().unwrap();
447 let config_path = dir.path().join("config.toml");
448
449 let toml_content = r#"
450name = "test-service"
451port = 3000
452debug = true
453
454[database]
455host = "db.example.com"
456port = 5432
457username = "testuser"
458password = "testpass"
459"#;
460
461 fs::write(&config_path, toml_content).unwrap();
462
463 let config: TestConfig = ConfigManager::load_from_file(&config_path).unwrap();
464
465 assert_eq!(config.name, "test-service");
466 assert_eq!(config.port, 3000);
467 assert_eq!(config.debug, true);
468 assert_eq!(config.database.host, "db.example.com");
469 }
470
471 #[test]
472 fn test_load_json_config() {
473 let dir = tempdir().unwrap();
474 let config_path = dir.path().join("config.json");
475
476 let json_content = r#"{
477 "name": "test-service",
478 "port": 3000,
479 "debug": true,
480 "database": {
481 "host": "db.example.com",
482 "port": 5432,
483 "username": "testuser",
484 "password": "testpass"
485 }
486}"#;
487
488 fs::write(&config_path, json_content).unwrap();
489
490 let config: TestConfig = ConfigManager::load_from_file(&config_path).unwrap();
491
492 assert_eq!(config.name, "test-service");
493 assert_eq!(config.port, 3000);
494 assert_eq!(config.debug, true);
495 assert_eq!(config.database.host, "db.example.com");
496 }
497
498 #[test]
499 fn test_config_builder_with_defaults() {
500 let config: TestConfig = ConfigBuilder::new()
501 .with_default("name", "default-app")
502 .unwrap()
503 .with_default("port", 9000)
504 .unwrap()
505 .with_default("debug", false)
506 .unwrap()
507 .with_default("database.host", "default-host")
508 .unwrap()
509 .with_default("database.port", 3306)
510 .unwrap()
511 .with_default("database.username", "default-user")
512 .unwrap()
513 .with_default("database.password", "default-pass")
514 .unwrap()
515 .build()
516 .unwrap();
517
518 assert_eq!(config.name, "default-app");
519 assert_eq!(config.port, 9000);
520 assert_eq!(config.debug, false);
521 assert_eq!(config.database.host, "default-host");
522 assert_eq!(config.database.port, 3306);
523 }
524
525 #[test]
526 fn test_config_with_env_override() {
527 let dir = tempdir().unwrap();
528 let config_path = dir.path().join("config.yaml");
529
530 let yaml_content = r#"
531name: "test-service"
532port: 3000
533debug: false
534database:
535 host: "localhost"
536 port: 5432
537 username: "user"
538 password: "password"
539"#;
540
541 fs::write(&config_path, yaml_content).unwrap();
542
543 unsafe {
545 env::set_var("TEST_PORT", "8080");
546 env::set_var("TEST_DEBUG", "true");
547 env::set_var("TEST_DATABASE__HOST", "env-db-host");
548 }
549
550 let config: TestConfig = ConfigManager::load_with_env(&config_path, "TEST").unwrap();
551
552 assert_eq!(config.name, "test-service"); assert_eq!(config.port, 8080); assert_eq!(config.debug, true); assert_eq!(config.database.host, "env-db-host"); unsafe {
559 env::remove_var("TEST_PORT");
560 env::remove_var("TEST_DEBUG");
561 env::remove_var("TEST_DATABASE__HOST");
562 }
563 }
564
565 #[test]
566 fn test_load_multiple_configs() {
567 let dir = tempdir().unwrap();
568
569 let base_config_path = dir.path().join("base.yaml");
571 let base_content = r#"
572name: "base-service"
573port: 8000
574debug: false
575database:
576 host: "base-host"
577 port: 5432
578 username: "base-user"
579 password: "base-pass"
580"#;
581 fs::write(&base_config_path, base_content).unwrap();
582
583 let override_config_path = dir.path().join("override.yaml");
585 let override_content = r#"
586port: 9000
587debug: true
588database:
589 host: "override-host"
590"#;
591 fs::write(&override_config_path, override_content).unwrap();
592
593 let config: TestConfig = ConfigManager::load_multiple(
594 vec![&base_config_path, &override_config_path],
595 None::<&str>,
596 )
597 .unwrap();
598
599 assert_eq!(config.name, "base-service"); assert_eq!(config.port, 9000); assert_eq!(config.debug, true); assert_eq!(config.database.host, "override-host"); assert_eq!(config.database.username, "base-user"); }
605
606 #[test]
607 fn test_get_config_paths() {
608 let paths = get_config_paths("myapp");
609
610 assert!(
611 paths
612 .iter()
613 .any(|p| p.to_string_lossy().ends_with("myapp.yaml"))
614 );
615 assert!(
616 paths
617 .iter()
618 .any(|p| p.to_string_lossy().ends_with("myapp.yml"))
619 );
620 assert!(
621 paths
622 .iter()
623 .any(|p| p.to_string_lossy().ends_with("myapp.toml"))
624 );
625 assert!(
626 paths
627 .iter()
628 .any(|p| p.to_string_lossy().ends_with("myapp.json"))
629 );
630 assert!(paths.iter().any(|p| p.to_string_lossy().contains("config")
631 && p.to_string_lossy().ends_with("myapp.yaml")));
632 }
633
634 #[test]
635 fn test_ignore_missing_files() {
636 let dir = tempdir().unwrap();
637 let existing_config = dir.path().join("existing.yaml");
638 let missing_config = dir.path().join("missing.yaml");
639
640 let yaml_content = r#"
641name: "test-service"
642port: 3000
643debug: true
644database:
645 host: "localhost"
646 port: 5432
647 username: "user"
648 password: "password"
649"#;
650
651 fs::write(&existing_config, yaml_content).unwrap();
652
653 let result: Result<TestConfig> = ConfigBuilder::new()
655 .add_file(&existing_config, None)
656 .add_file(&missing_config, None)
657 .ignore_missing_files(false)
658 .build();
659 assert!(result.is_err());
660
661 let config: TestConfig = ConfigBuilder::new()
663 .add_file(&existing_config, None)
664 .add_file(&missing_config, None)
665 .ignore_missing_files(true)
666 .build()
667 .unwrap();
668
669 assert_eq!(config.name, "test-service");
670 }
671}