clamber_core/config/
mod.rs

1//! 配置管理模块:支持多格式配置文件(YAML/TOML/JSON)、环境变量覆盖(可自定义前缀与分隔符)、多文件合并与默认值。
2//! 参见项目根目录的 CONFIG.md 获取更完整的使用指南与示例。
3use 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/// 配置文件格式枚举
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ConfigFormat {
13    /// YAML 格式
14    Yaml,
15    /// TOML 格式
16    Toml,
17    /// JSON 格式
18    Json,
19}
20
21impl ConfigFormat {
22    /// 从文件扩展名推断配置格式
23    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    /// 转换为 config crate 的 FileFormat
33    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/// 配置构建器
43#[derive(Debug, Clone)]
44pub struct ConfigBuilder {
45    /// 配置文件路径列表
46    files: Vec<(PathBuf, Option<ConfigFormat>)>,
47    /// 环境变量前缀
48    env_prefix: Option<String>,
49    /// 环境变量分隔符
50    env_separator: String,
51    /// 是否忽略缺失的配置文件
52    ignore_missing: bool,
53    /// 默认值
54    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    /// 创建新的配置构建器
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// 添加配置文件
76    ///
77    /// # 参数
78    /// * `path` - 配置文件路径
79    /// * `format` - 可选的文件格式,如果不指定则从文件扩展名推断
80    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    /// 添加 YAML 配置文件
86    pub fn add_yaml_file<P: AsRef<Path>>(self, path: P) -> Self {
87        self.add_file(path, Some(ConfigFormat::Yaml))
88    }
89
90    /// 添加 TOML 配置文件
91    pub fn add_toml_file<P: AsRef<Path>>(self, path: P) -> Self {
92        self.add_file(path, Some(ConfigFormat::Toml))
93    }
94
95    /// 添加 JSON 配置文件
96    pub fn add_json_file<P: AsRef<Path>>(self, path: P) -> Self {
97        self.add_file(path, Some(ConfigFormat::Json))
98    }
99
100    /// 设置环境变量前缀
101    ///
102    /// # 参数
103    /// * `prefix` - 环境变量前缀,例如 "APP"
104    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    /// 设置环境变量分隔符
110    ///
111    /// # 参数
112    /// * `separator` - 分隔符,默认为 "__"
113    pub fn with_env_separator<S: Into<String>>(mut self, separator: S) -> Self {
114        self.env_separator = separator.into();
115        self
116    }
117
118    /// 设置是否忽略缺失的配置文件
119    pub fn ignore_missing_files(mut self, ignore: bool) -> Self {
120        self.ignore_missing = ignore;
121        self
122    }
123
124    /// 添加默认值
125    ///
126    /// # 参数
127    /// * `key` - 配置键
128    /// * `value` - 默认值
129    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    /// 构建配置并反序列化为指定类型
139    ///
140    /// # 返回值
141    /// 返回反序列化后的配置对象
142    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        // 添加默认值
149        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        // 添加配置文件
158        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        // 添加环境变量
173        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        // 构建配置
182        let config = config_builder
183            .build()
184            .map_err(|e| ClamberError::ConfigLoadError {
185                details: e.to_string(),
186            })?;
187
188        // 反序列化
189        config
190            .try_deserialize::<T>()
191            .map_err(|e| ClamberError::ConfigParseError {
192                details: e.to_string(),
193            })
194    }
195
196    /// 构建配置并返回原始 Config 对象
197    pub fn build_raw(self) -> Result<Config> {
198        let mut config_builder = Config::builder();
199
200        // 添加默认值
201        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        // 添加配置文件
210        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        // 添加环境变量
225        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        // 构建配置
234        config_builder
235            .build()
236            .map_err(|e| ClamberError::ConfigLoadError {
237                details: e.to_string(),
238            })
239    }
240}
241
242/// 配置管理器
243pub struct ConfigManager;
244
245impl ConfigManager {
246    /// 从单个配置文件加载配置
247    ///
248    /// # 参数
249    /// * `path` - 配置文件路径
250    ///
251    /// # 返回值
252    /// 返回反序列化后的配置对象
253    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    /// 从配置文件和环境变量加载配置
262    ///
263    /// # 参数
264    /// * `config_path` - 配置文件路径
265    /// * `env_prefix` - 环境变量前缀
266    ///
267    /// # 返回值
268    /// 返回反序列化后的配置对象
269    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    /// 加载多个配置文件,支持环境变量覆盖
282    ///
283    /// # 参数
284    /// * `config_paths` - 配置文件路径列表(按优先级顺序)
285    /// * `env_prefix` - 可选的环境变量前缀
286    ///
287    /// # 返回值
288    /// 返回反序列化后的配置对象
289    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    /// 创建配置构建器
309    pub fn builder() -> ConfigBuilder {
310        ConfigBuilder::new()
311    }
312}
313
314/// 便利函数:从配置文件加载配置
315pub 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
323/// 便利函数:从配置文件和环境变量加载配置
324pub 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
333/// 便利函数:获取当前工作目录下的配置文件路径
334pub 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
349/// 便利函数:自动发现并加载配置文件
350pub 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        // 设置环境变量
544        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"); // 从文件
553        assert_eq!(config.port, 8080); // 从环境变量覆盖
554        assert_eq!(config.debug, true); // 从环境变量覆盖
555        assert_eq!(config.database.host, "env-db-host"); // 从环境变量覆盖
556
557        // 清理环境变量
558        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        // 创建基础配置文件
570        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        // 创建覆盖配置文件
584        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"); // 从基础配置
600        assert_eq!(config.port, 9000); // 被覆盖
601        assert_eq!(config.debug, true); // 被覆盖
602        assert_eq!(config.database.host, "override-host"); // 被覆盖
603        assert_eq!(config.database.username, "base-user"); // 从基础配置
604    }
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        // 不忽略缺失文件时应该失败
654        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        // 忽略缺失文件时应该成功
662        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}