gonfig/environment.rs
1use crate::{
2 error::Result,
3 source::{ConfigSource, Source},
4 Prefix,
5};
6use serde_json::{json, Map, Value};
7use std::any::Any;
8use std::collections::HashMap;
9use std::env;
10
11/// Environment variable configuration source.
12///
13/// The `Environment` struct provides a flexible way to read configuration values
14/// from environment variables. It supports prefixes, custom separators, case sensitivity
15/// control, and field-specific mappings.
16///
17/// # Examples
18///
19/// ## Basic Usage
20///
21/// ```rust
22/// use gonfig::{Environment, ConfigBuilder};
23/// use serde::Deserialize;
24///
25/// #[derive(Deserialize)]
26/// struct Config {
27/// database_url: String,
28/// port: u16,
29/// }
30///
31/// std::env::set_var("APP_DATABASE_URL", "postgres://localhost/db");
32/// std::env::set_var("APP_PORT", "5432");
33///
34/// let config: Config = ConfigBuilder::new()
35/// .add_source(Box::new(Environment::new().with_prefix("APP")))
36/// .build()
37/// .unwrap();
38/// ```
39///
40/// ## Advanced Configuration
41///
42/// ```rust
43/// use gonfig::Environment;
44///
45/// let env = Environment::new()
46/// .with_prefix("MYAPP")
47/// .separator("__") // Use double underscore
48/// .case_sensitive(true)
49/// .override_with("database_url", "postgres://override/db")
50/// .with_field_mapping("db_url", "CUSTOM_DB_CONNECTION");
51/// ```
52#[derive(Debug, Clone)]
53pub struct Environment {
54 prefix: Option<Prefix>,
55 separator: String,
56 case_sensitive: bool,
57 overrides: HashMap<String, String>,
58 field_mappings: HashMap<String, String>,
59 nested: bool,
60}
61
62impl Default for Environment {
63 fn default() -> Self {
64 Self {
65 prefix: None,
66 separator: "_".to_string(),
67 case_sensitive: false,
68 overrides: HashMap::new(),
69 field_mappings: HashMap::new(),
70 nested: false,
71 }
72 }
73}
74
75impl Environment {
76 /// Create a new environment variable source with default settings.
77 ///
78 /// Default configuration:
79 /// - No prefix
80 /// - Separator: `"_"`
81 /// - Case sensitive: `false` (environment variables are converted to uppercase)
82 /// - No overrides or field mappings
83 ///
84 /// # Examples
85 ///
86 /// ```rust
87 /// use gonfig::Environment;
88 ///
89 /// let env = Environment::new();
90 /// ```
91 pub fn new() -> Self {
92 Self::default()
93 }
94
95 /// Set the environment variable prefix.
96 ///
97 /// When a prefix is set, environment variables will be expected in the format
98 /// `{PREFIX}{SEPARATOR}{FIELD_NAME}`. For example, with prefix "APP" and
99 /// separator "_", a field named `database_url` would map to `APP_DATABASE_URL`.
100 ///
101 /// # Examples
102 ///
103 /// ```rust
104 /// use gonfig::Environment;
105 ///
106 /// let env = Environment::new().with_prefix("MYAPP");
107 /// // Will look for MYAPP_* environment variables
108 /// ```
109 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
110 self.prefix = Some(Prefix::new(prefix));
111 self
112 }
113
114 /// Set the separator used between prefix and field names.
115 ///
116 /// The default separator is `"_"`. This affects how environment variable
117 /// names are constructed from the prefix and field names.
118 ///
119 /// # Examples
120 ///
121 /// ```rust
122 /// use gonfig::Environment;
123 ///
124 /// let env = Environment::new()
125 /// .with_prefix("APP")
126 /// .separator("__"); // Results in APP__FIELD_NAME
127 /// ```
128 pub fn separator(mut self, sep: impl Into<String>) -> Self {
129 self.separator = sep.into();
130 self
131 }
132
133 /// Control case sensitivity for environment variable names.
134 ///
135 /// When `false` (default), all environment variable names are converted
136 /// to uppercase. When `true`, the exact case is preserved.
137 ///
138 /// # Examples
139 ///
140 /// ```rust
141 /// use gonfig::Environment;
142 ///
143 /// let env = Environment::new()
144 /// .with_prefix("app")
145 /// .case_sensitive(true);
146 /// // Will look for app_field_name instead of APP_FIELD_NAME
147 /// ```
148 pub fn case_sensitive(mut self, sensitive: bool) -> Self {
149 self.case_sensitive = sensitive;
150 self
151 }
152
153 /// Override a specific field with a hardcoded value.
154 ///
155 /// This is useful for providing default values or overriding environment
156 /// variables programmatically. Overrides take precedence over actual
157 /// environment variables.
158 ///
159 /// # Examples
160 ///
161 /// ```rust
162 /// use gonfig::Environment;
163 ///
164 /// let env = Environment::new()
165 /// .override_with("debug", "true")
166 /// .override_with("timeout", "30");
167 /// ```
168 pub fn override_with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
169 self.overrides.insert(key.into(), value.into());
170 self
171 }
172
173 /// Map a specific field to a custom environment variable name.
174 ///
175 /// This allows you to override the default environment variable naming
176 /// for specific fields. The mapping takes precedence over the standard
177 /// prefix and separator rules.
178 ///
179 /// # Examples
180 ///
181 /// ```rust
182 /// use gonfig::Environment;
183 ///
184 /// let env = Environment::new()
185 /// .with_prefix("APP")
186 /// .with_field_mapping("database_url", "DATABASE_CONNECTION_STRING");
187 /// // database_url will read from DATABASE_CONNECTION_STRING instead of APP_DATABASE_URL
188 /// ```
189 pub fn with_field_mapping(
190 mut self,
191 field_name: impl Into<String>,
192 env_key: impl Into<String>,
193 ) -> Self {
194 self.field_mappings
195 .insert(field_name.into(), env_key.into());
196 self
197 }
198
199 /// Enable nested mode to convert flat environment variable keys into nested structures.
200 ///
201 /// When enabled, environment variables with the configured separator (default: `_`) will be split
202 /// into nested paths. For example, `APP_HTTP_PORT=9000` becomes `{"http": {"port": 9000}}`.
203 ///
204 /// This is essential for properly overriding nested configuration file values with
205 /// environment variables when using the Deep merge strategy.
206 ///
207 /// # Examples
208 ///
209 /// ```rust
210 /// use gonfig::{Environment, ConfigBuilder, MergeStrategy};
211 ///
212 /// // With nested=true, APP_HTTP_PORT will override http.port in config file
213 /// let env = Environment::new()
214 /// .with_prefix("APP")
215 /// .nested(true);
216 /// ```
217 pub fn nested(mut self, nested: bool) -> Self {
218 self.nested = nested;
219 self
220 }
221
222 fn build_env_key(&self, path: &[&str]) -> String {
223 let mut parts = Vec::new();
224
225 if let Some(prefix) = &self.prefix {
226 parts.push(prefix.as_str().to_string());
227 }
228
229 for part in path {
230 parts.push(part.to_string());
231 }
232
233 let key = parts.join(&self.separator);
234
235 if self.case_sensitive {
236 key
237 } else {
238 key.to_uppercase()
239 }
240 }
241
242 /// Normalize a key for storage in the flat map based on nested mode setting.
243 ///
244 /// In nested mode, preserves the original case for proper splitting.
245 /// In flat mode, converts to lowercase for backward compatibility.
246 fn normalize_key(&self, key: &str) -> String {
247 if self.nested {
248 key.to_string()
249 } else {
250 key.to_lowercase()
251 }
252 }
253
254 fn parse_env_value(value: &str) -> Value {
255 if let Ok(b) = value.parse::<bool>() {
256 return json!(b);
257 }
258
259 if let Ok(n) = value.parse::<i64>() {
260 return json!(n);
261 }
262
263 if let Ok(n) = value.parse::<f64>() {
264 return json!(n);
265 }
266
267 if value.starts_with('[') && value.ends_with(']') {
268 if let Ok(arr) = serde_json::from_str::<Vec<Value>>(value) {
269 return json!(arr);
270 }
271 }
272
273 if value.starts_with('{') && value.ends_with('}') {
274 if let Ok(obj) = serde_json::from_str::<Value>(value) {
275 return obj;
276 }
277 }
278
279 json!(value)
280 }
281
282 /// Recursively insert a value into a nested map structure based on a path of keys.
283 ///
284 /// This helper function takes a flat key path (e.g., ["http", "server", "port"])
285 /// and creates the necessary nested structure in the map, inserting the value
286 /// at the deepest level.
287 fn insert_nested(map: &mut Map<String, Value>, parts: &[String], value: Value) {
288 if parts.is_empty() {
289 return;
290 }
291
292 if parts.len() == 1 {
293 // Base case: insert the value at this key
294 map.insert(parts[0].clone(), value);
295 return;
296 }
297
298 // Recursive case: get or create the nested object
299 let key = parts[0].clone();
300 match map.entry(key) {
301 serde_json::map::Entry::Occupied(mut occ) => {
302 if let Value::Object(ref mut nested) = occ.get_mut() {
303 Self::insert_nested(nested, &parts[1..], value);
304 } else {
305 // Replace non-object with a new object containing the nested value
306 let mut new_map = Map::new();
307 Self::insert_nested(&mut new_map, &parts[1..], value);
308 *occ.get_mut() = Value::Object(new_map);
309 }
310 }
311 serde_json::map::Entry::Vacant(vac) => {
312 let mut new_map = Map::new();
313 Self::insert_nested(&mut new_map, &parts[1..], value);
314 vac.insert(Value::Object(new_map));
315 }
316 }
317 }
318
319 pub fn collect_for_struct(
320 &self,
321 struct_name: &str,
322 fields: &[(&str, Option<&str>)],
323 ) -> HashMap<String, Value> {
324 let mut result = HashMap::new();
325
326 for (field_name, field_override) in fields {
327 let env_key = if let Some(override_name) = field_override {
328 override_name.to_string()
329 } else if let Some(prefix) = &self.prefix {
330 format!(
331 "{}_{}_{}_{}",
332 prefix.as_str().to_uppercase(),
333 struct_name.to_uppercase(),
334 field_name.to_uppercase(),
335 ""
336 )
337 .trim_end_matches('_')
338 .to_string()
339 } else {
340 format!(
341 "{}_{}",
342 struct_name.to_uppercase(),
343 field_name.to_uppercase()
344 )
345 };
346
347 if let Some(override_value) = self.overrides.get(&env_key) {
348 result.insert(
349 field_name.to_string(),
350 Self::parse_env_value(override_value),
351 );
352 } else if let Ok(value) = env::var(&env_key) {
353 result.insert(field_name.to_string(), Self::parse_env_value(&value));
354 }
355 }
356
357 result
358 }
359
360 pub fn collect_with_flat_keys(&self) -> Result<Value> {
361 let mut flat_map = HashMap::new();
362
363 // First collect from environment variables
364 for (key, value) in env::vars() {
365 if let Some(prefix) = &self.prefix {
366 let prefix_str = if self.case_sensitive {
367 prefix.as_str().to_string()
368 } else {
369 prefix.as_str().to_uppercase()
370 };
371
372 let key_check = if self.case_sensitive {
373 key.clone()
374 } else {
375 key.to_uppercase()
376 };
377
378 if key_check.starts_with(&prefix_str) {
379 let trimmed = key_check[prefix_str.len()..].trim_start_matches(&self.separator);
380 let key_for_map = self.normalize_key(trimmed);
381 flat_map.insert(key_for_map, Self::parse_env_value(&value));
382 }
383 } else {
384 flat_map.insert(key.to_lowercase(), Self::parse_env_value(&value));
385 }
386 }
387
388 // Then apply overrides (overrides take precedence)
389 for (override_key, override_value) in &self.overrides {
390 if let Some(prefix) = &self.prefix {
391 let prefix_str = if self.case_sensitive {
392 prefix.as_str().to_string()
393 } else {
394 prefix.as_str().to_uppercase()
395 };
396
397 let key_check = if self.case_sensitive {
398 override_key.clone()
399 } else {
400 override_key.to_uppercase()
401 };
402
403 if key_check.starts_with(&prefix_str) {
404 let trimmed = key_check[prefix_str.len()..].trim_start_matches(&self.separator);
405 let key_for_map = self.normalize_key(trimmed);
406 flat_map.insert(key_for_map, Self::parse_env_value(override_value));
407 }
408 } else {
409 flat_map.insert(
410 override_key.to_lowercase(),
411 Self::parse_env_value(override_value),
412 );
413 }
414 }
415
416 // Convert flat keys into nested structures if enabled
417 let mut result = Map::new();
418 for (key, value) in flat_map {
419 if self.nested {
420 // Split on separator to create nested structure
421 let parts: Vec<&str> = key.split(&self.separator).collect();
422 if parts.len() == 1 {
423 // Single part, insert directly (lowercase it)
424 result.insert(key.to_lowercase(), value);
425 } else {
426 // Multiple parts, create nested structure
427 // Lowercase each part individually
428 let lowercase_parts: Vec<String> =
429 parts.iter().map(|p| p.to_lowercase()).collect();
430 Self::insert_nested(&mut result, &lowercase_parts, value);
431 }
432 } else {
433 // Keep keys flat (backward compatible behavior)
434 result.insert(key.to_lowercase(), value);
435 }
436 }
437
438 Ok(Value::Object(result))
439 }
440}
441
442impl ConfigSource for Environment {
443 fn source_type(&self) -> Source {
444 Source::Environment
445 }
446
447 fn collect(&self) -> Result<Value> {
448 if !self.field_mappings.is_empty() {
449 // Use field mappings when available
450 let mut result = Map::new();
451
452 // First collect using field mappings
453 for (field_name, env_key) in &self.field_mappings {
454 // Check overrides first, then environment
455 if let Some(override_value) = self.overrides.get(env_key) {
456 result.insert(field_name.clone(), Self::parse_env_value(override_value));
457 } else if let Ok(value) = env::var(env_key) {
458 result.insert(field_name.clone(), Self::parse_env_value(&value));
459 }
460 }
461
462 // Then collect any prefixed variables not in mappings
463 if let Some(prefix) = &self.prefix {
464 for (key, value) in env::vars() {
465 let prefix_str = if self.case_sensitive {
466 prefix.as_str().to_string()
467 } else {
468 prefix.as_str().to_uppercase()
469 };
470
471 let key_check = if self.case_sensitive {
472 key.clone()
473 } else {
474 key.to_uppercase()
475 };
476
477 if key_check.starts_with(&prefix_str)
478 && !self.field_mappings.values().any(|v| v == &key)
479 {
480 let trimmed =
481 key_check[prefix_str.len()..].trim_start_matches(&self.separator);
482 let field_name = trimmed.to_lowercase();
483 if !result.contains_key(&field_name) {
484 result.insert(field_name, Self::parse_env_value(&value));
485 }
486 }
487 }
488 }
489
490 Ok(Value::Object(result))
491 } else {
492 self.collect_with_flat_keys()
493 }
494 }
495
496 fn has_value(&self, key: &str) -> bool {
497 let env_key = self.build_env_key(&[key]);
498 self.overrides.contains_key(&env_key) || env::var(&env_key).is_ok()
499 }
500
501 fn get_value(&self, key: &str) -> Option<Value> {
502 let env_key = self.build_env_key(&[key]);
503
504 if let Some(override_value) = self.overrides.get(&env_key) {
505 Some(Self::parse_env_value(override_value))
506 } else {
507 env::var(&env_key).ok().map(|v| Self::parse_env_value(&v))
508 }
509 }
510
511 fn as_any(&self) -> &dyn Any {
512 self
513 }
514}