sz-orm-config 1.2.2

Config management: in-memory config center with hot-reload API (file/env/etcd/consul data sources not yet integrated)
Documentation
//! 环境变量配置源:从系统环境变量读取配置,支持前缀过滤与嵌套键映射。

use crate::{ConfigError, ConfigSource};
use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
use std::time::Duration;

/// 环境变量配置源
///
/// 从系统环境变量读取配置:
/// - **前缀过滤**:仅读取以 `prefix` 开头的环境变量,并去除前缀。
/// - **嵌套键映射**:将分隔符(默认 `__`)替换为点号,实现嵌套键映射。
///   例如前缀 `APP_` 下,`APP_DB__HOST=localhost` -> 键 `db.host`。
/// - 键统一转小写。
/// - **类型推断**:值自动推断为布尔/整数/浮点数/字符串,
///   例如 `APP_PORT=3306` -> `Value::Number(3306)`,`APP_DEBUG=true` -> `Value::Bool(true)`。
///
/// 环境变量无原生变更通知,`watch` 通过轮询实现(默认 5 秒)。
pub struct EnvConfigSource {
    /// 环境变量前缀(如 `APP_`)
    prefix: String,
    /// 嵌套分隔符(默认 `__`)
    separator: String,
    /// watch 轮询间隔
    poll_interval: Duration,
}

impl EnvConfigSource {
    /// 创建环境变量配置源
    ///
    /// - `prefix`: 环境变量前缀,例如 `APP_`。仅读取以此前缀开头的变量。
    pub fn new(prefix: impl Into<String>) -> Self {
        Self {
            prefix: prefix.into(),
            separator: "__".to_string(),
            poll_interval: Duration::from_secs(5),
        }
    }

    /// 设置嵌套分隔符(默认 `__`),分隔符会被替换为点号
    pub fn with_separator(mut self, sep: impl Into<String>) -> Self {
        self.separator = sep.into();
        self
    }

    /// 设置 `watch` 轮询间隔
    pub fn with_poll_interval(mut self, interval: Duration) -> Self {
        self.poll_interval = interval;
        self
    }

    /// 收集当前环境变量并映射为配置 map
    ///
    /// 对每个以 `prefix` 开头的环境变量:
    /// 1. 去除前缀;
    /// 2. 转小写;
    /// 3. 将分隔符替换为点号(实现嵌套键映射);
    /// 4. 对值进行类型推断(布尔/整数/浮点数/字符串)。
    fn collect(&self) -> HashMap<String, Value> {
        let mut map = HashMap::new();
        for (key, value) in std::env::vars() {
            if let Some(stripped) = key.strip_prefix(&self.prefix) {
                let mapped = stripped
                    .to_lowercase()
                    .replace(&self.separator, ".");
                map.insert(mapped, crate::parse_config_value(&value));
            }
        }
        map
    }
}

#[async_trait]
impl ConfigSource for EnvConfigSource {
    /// 加载环境变量配置
    async fn load(&self) -> Result<HashMap<String, Value>, ConfigError> {
        Ok(self.collect())
    }

    /// 轮询监听环境变量变化
    ///
    /// 在独立线程中按固定间隔读取环境变量,与上次快照比较,
    /// 检测到任意键的新增/修改/删除时,将最新全量配置通过回调返回。
    async fn watch(
        &self,
        callback: Box<dyn Fn(HashMap<String, Value>) + Send + Sync>,
    ) -> Result<(), ConfigError> {
        let prefix = self.prefix.clone();
        let separator = self.separator.clone();
        let interval = self.poll_interval;
        std::thread::spawn(move || {
            let cfg = EnvConfigSource {
                prefix,
                separator,
                poll_interval: interval,
            };
            let mut last = cfg.collect();
            loop {
                std::thread::sleep(interval);
                let current = cfg.collect();
                if current != last {
                    last = current.clone();
                    callback(current);
                }
            }
        });
        Ok(())
    }
}
#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_load_with_prefix_and_nested_mapping() {
        // 使用唯一前缀避免与其他测试/进程冲突
        std::env::set_var("SZORMCFG_TEST_DB__HOST", "localhost");
        std::env::set_var("SZORMCFG_TEST_DB__PORT", "3306");
        std::env::set_var("SZORMCFG_TEST_DEBUG", "true");
        std::env::set_var("SZORMCFG_TEST_OTHER_VAR", "ignored");
        let src = EnvConfigSource::new("SZORMCFG_TEST_");
        let data = src.load().await.unwrap();
        assert_eq!(data.get("db.host"), Some(&Value::from("localhost")));
        assert_eq!(data.get("db.port"), Some(&Value::from(3306)));
        assert_eq!(data.get("debug"), Some(&Value::from(true)));
        // 单下划线不会被分隔符 __ 替换,键保持为 other_var
        assert_eq!(data.get("other_var"), Some(&Value::from("ignored")));
        // 不带前缀的变量不应出现
        assert!(data.get("nonexistent").is_none());
        // 清理
        std::env::remove_var("SZORMCFG_TEST_DB__HOST");
        std::env::remove_var("SZORMCFG_TEST_DB__PORT");
        std::env::remove_var("SZORMCFG_TEST_DEBUG");
        std::env::remove_var("SZORMCFG_TEST_OTHER_VAR");
    }

    #[tokio::test]
    async fn test_load_custom_separator() {
        std::env::set_var("SZORMCFG_SEP_DB__HOST", "h");
        let src = EnvConfigSource::new("SZORMCFG_SEP_").with_separator("__");
        let data = src.load().await.unwrap();
        assert_eq!(data.get("db.host"), Some(&Value::from("h")));
        std::env::remove_var("SZORMCFG_SEP_DB__HOST");
    }

    #[tokio::test]
    async fn test_load_no_matching_vars() {
        let src = EnvConfigSource::new("DEFINITELY_NONEXISTENT_PREFIX_XYZ_");
        let data = src.load().await.unwrap();
        assert!(data.is_empty());
    }

    #[tokio::test]
    async fn test_load_strips_prefix_and_lowercases() {
        std::env::set_var("SZORMCFG_CASE_HOST", "H1");
        let src = EnvConfigSource::new("SZORMCFG_CASE_");
        let data = src.load().await.unwrap();
        // 前缀已去除且键转小写
        assert_eq!(data.get("host"), Some(&Value::from("H1")));
        std::env::remove_var("SZORMCFG_CASE_HOST");
    }
}