distributed_config/sources/
env.rs1use 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
11pub struct EnvSource {
13 prefix: Option<String>,
14 separator: String,
15 case_sensitive: bool,
16 name: String,
17}
18
19impl EnvSource {
20 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 pub fn prefix<S: Into<String>>(mut self, prefix: S) -> Self {
32 self.prefix = Some(prefix.into());
33 self
34 }
35
36 pub fn separator<S: Into<String>>(mut self, separator: S) -> Self {
38 self.separator = separator.into();
39 self
40 }
41
42 pub fn case_sensitive(mut self, case_sensitive: bool) -> Self {
44 self.case_sensitive = case_sensitive;
45 self
46 }
47
48 pub fn with_name<S: Into<String>>(mut self, name: S) -> Self {
50 self.name = name.into();
51 self
52 }
53
54 fn env_to_key(&self, env_name: &str) -> Option<String> {
56 let mut key = env_name.to_string();
57
58 if let Some(prefix) = &self.prefix {
60 if key.starts_with(prefix) {
61 key = key[prefix.len()..].to_string();
62 } else {
63 return None; }
65 }
66
67 if !self.case_sensitive {
69 key = key.to_lowercase();
70 }
71
72 key = key.replace(&self.separator, ".");
74
75 Some(key)
76 }
77
78 fn parse_env_value(&self, value: &str) -> ConfigValue {
80 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 if let Ok(int_val) = value.parse::<i64>() {
91 return ConfigValue::Integer(int_val);
92 }
93
94 if let Ok(float_val) = value.parse::<f64>() {
96 return ConfigValue::Float(float_val);
97 }
98
99 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 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 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 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 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 }
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 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 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 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 unsafe {
287 env::remove_var("TEST_JSON");
288 }
289 }
290}