clamber_core/config/
mod.rs

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