distributed_config/sources/
env.rs

1//! Environment variable configuration source
2
3use crate::error::{ConfigError, Result};
4use crate::sources::ConfigSource;
5use crate::value::ConfigValue;
6use async_trait::async_trait;
7use std::collections::HashMap;
8use std::env;
9use tracing::{debug, info};
10
11/// Configuration source that loads from environment variables
12pub struct EnvSource {
13    prefix: Option<String>,
14    separator: String,
15    case_sensitive: bool,
16    name: String,
17}
18
19impl EnvSource {
20    /// Create a new environment source
21    pub fn new() -> Self {
22        Self {
23            prefix: None,
24            separator: "__".to_string(),
25            case_sensitive: false,
26            name: "env".to_string(),
27        }
28    }
29
30    /// Set a prefix for environment variables (e.g., "APP_")
31    pub fn prefix<S: Into<String>>(mut self, prefix: S) -> Self {
32        self.prefix = Some(prefix.into());
33        self
34    }
35
36    /// Set the separator used to create nested keys (default: "__")
37    pub fn separator<S: Into<String>>(mut self, separator: S) -> Self {
38        self.separator = separator.into();
39        self
40    }
41
42    /// Set whether environment variable names are case-sensitive
43    pub fn case_sensitive(mut self, case_sensitive: bool) -> Self {
44        self.case_sensitive = case_sensitive;
45        self
46    }
47
48    /// Set the name of this source
49    pub fn with_name<S: Into<String>>(mut self, name: S) -> Self {
50        self.name = name.into();
51        self
52    }
53
54    /// Convert an environment variable name to a configuration key
55    fn env_to_key(&self, env_name: &str) -> Option<String> {
56        let mut key = env_name.to_string();
57
58        // Remove prefix if present
59        if let Some(prefix) = &self.prefix {
60            if key.starts_with(prefix) {
61                key = key[prefix.len()..].to_string();
62            } else {
63                return None; // Skip variables that don't match the prefix
64            }
65        }
66
67        // Convert to lowercase if not case-sensitive
68        if !self.case_sensitive {
69            key = key.to_lowercase();
70        }
71
72        // Replace separator with dots for nested keys
73        key = key.replace(&self.separator, ".");
74
75        Some(key)
76    }
77
78    /// Parse environment variable value into ConfigValue
79    fn parse_env_value(&self, value: &str) -> ConfigValue {
80        // Try to parse as various types
81
82        // Boolean
83        match value.to_lowercase().as_str() {
84            "true" | "yes" | "1" | "on" => return ConfigValue::Bool(true),
85            "false" | "no" | "0" | "off" => return ConfigValue::Bool(false),
86            _ => {}
87        }
88
89        // Integer
90        if let Ok(int_val) = value.parse::<i64>() {
91            return ConfigValue::Integer(int_val);
92        }
93
94        // Float
95        if let Ok(float_val) = value.parse::<f64>() {
96            return ConfigValue::Float(float_val);
97        }
98
99        // JSON (for complex types)
100        if value.starts_with('{') && value.ends_with('}') {
101            if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(value) {
102                return ConfigValue::from(json_val);
103            }
104        }
105
106        // JSON Array
107        if value.starts_with('[') && value.ends_with(']') {
108            if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(value) {
109                return ConfigValue::from(json_val);
110            }
111        }
112
113        // Comma-separated list
114        if value.contains(',') {
115            let items: Vec<ConfigValue> = value
116                .split(',')
117                .map(|s| self.parse_env_value(s.trim()))
118                .collect();
119            return ConfigValue::Array(items);
120        }
121
122        // Default to string
123        ConfigValue::String(value.to_string())
124    }
125}
126
127impl Default for EnvSource {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133#[async_trait]
134impl ConfigSource for EnvSource {
135    async fn load(&self) -> Result<ConfigValue> {
136        let mut config = ConfigValue::Object(HashMap::new());
137
138        info!("Loading configuration from environment variables");
139
140        // Get all environment variables
141        let env_vars: Vec<(String, String)> = env::vars().collect();
142        let mut processed_count = 0;
143
144        for (env_name, env_value) in env_vars {
145            if let Some(config_key) = self.env_to_key(&env_name) {
146                let config_value = self.parse_env_value(&env_value);
147
148                debug!(
149                    "Mapping env var {} -> {}: {:?}",
150                    env_name, config_key, config_value
151                );
152
153                if let Err(e) = config.set_path(&config_key, config_value) {
154                    return Err(ConfigError::Other(format!(
155                        "Failed to set config path '{config_key}' from env var '{env_name}': {e}"
156                    )));
157                }
158
159                processed_count += 1;
160            }
161        }
162
163        info!("Processed {} environment variables", processed_count);
164
165        Ok(config)
166    }
167
168    fn name(&self) -> &str {
169        &self.name
170    }
171
172    fn supports_watching(&self) -> bool {
173        false // Environment variables don't typically change during runtime
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use std::env;
181
182    #[tokio::test]
183    async fn test_env_source_basic() {
184        unsafe {
185            env::set_var("TEST_KEY", "test_value");
186            env::set_var("TEST_NUMBER", "42");
187            env::set_var("TEST_BOOL", "true");
188        }
189
190        let source = EnvSource::new().prefix("TEST_");
191        let config = source.load().await.unwrap();
192
193        assert_eq!(
194            config.get_path("key").unwrap().as_string().unwrap(),
195            "test_value"
196        );
197        assert_eq!(config.get_path("number").unwrap().as_integer().unwrap(), 42);
198        assert!(config.get_path("bool").unwrap().as_bool().unwrap());
199
200        // Clean up
201        unsafe {
202            env::remove_var("TEST_KEY");
203            env::remove_var("TEST_NUMBER");
204            env::remove_var("TEST_BOOL");
205        }
206    }
207
208    #[tokio::test]
209    async fn test_env_source_nested() {
210        unsafe {
211            env::set_var("APP_DATABASE__HOST", "localhost");
212            env::set_var("APP_DATABASE__PORT", "5432");
213        }
214
215        let source = EnvSource::new().prefix("APP_").separator("__");
216        let config = source.load().await.unwrap();
217
218        assert_eq!(
219            config
220                .get_path("database.host")
221                .unwrap()
222                .as_string()
223                .unwrap(),
224            "localhost"
225        );
226        assert_eq!(
227            config
228                .get_path("database.port")
229                .unwrap()
230                .as_integer()
231                .unwrap(),
232            5432
233        );
234
235        // Clean up
236        unsafe {
237            env::remove_var("APP_DATABASE__HOST");
238            env::remove_var("APP_DATABASE__PORT");
239        }
240    }
241
242    #[tokio::test]
243    async fn test_env_source_array() {
244        unsafe {
245            env::set_var("TEST_ARRAY", "item1,item2,item3");
246        }
247
248        let source = EnvSource::new().prefix("TEST_");
249        let config = source.load().await.unwrap();
250
251        let array = config.get_path("array").unwrap().as_array().unwrap();
252        assert_eq!(array.len(), 3);
253        assert_eq!(array[0].as_string().unwrap(), "item1");
254        assert_eq!(array[1].as_string().unwrap(), "item2");
255        assert_eq!(array[2].as_string().unwrap(), "item3");
256
257        // Clean up
258        unsafe {
259            env::remove_var("TEST_ARRAY");
260        }
261    }
262
263    #[tokio::test]
264    async fn test_env_source_json() {
265        unsafe {
266            env::set_var("TEST_JSON", r#"{"key": "value", "number": 42}"#);
267        }
268
269        let source = EnvSource::new().prefix("TEST_");
270        let config = source.load().await.unwrap();
271
272        assert_eq!(
273            config.get_path("json.key").unwrap().as_string().unwrap(),
274            "value"
275        );
276        assert_eq!(
277            config
278                .get_path("json.number")
279                .unwrap()
280                .as_integer()
281                .unwrap(),
282            42
283        );
284
285        // Clean up
286        unsafe {
287            env::remove_var("TEST_JSON");
288        }
289    }
290}