Skip to main content

qubit_config/source/
env_config_source.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! # System Environment Variable Configuration Source
10//!
11//! Loads configuration from the current process's environment variables.
12//!
13//! # Key Transformation
14//!
15//! When a prefix is set, only variables matching the prefix are loaded, and
16//! the prefix is stripped from the key name. The key is then lowercased and
17//! underscores are converted to dots to produce the config key.
18//!
19//! For example, with prefix `APP_`:
20//! - `APP_SERVER_HOST=localhost` → `server.host = "localhost"`
21//! - `APP_SERVER_PORT=8080` → `server.port = "8080"`
22//!
23//! Without a prefix, all environment variables are loaded as-is.
24//!
25//! # Author
26//!
27//! Haixing Hu
28
29use std::ffi::{OsStr, OsString};
30
31use crate::{Config, ConfigError, ConfigResult};
32
33use super::ConfigSource;
34
35/// Configuration source that loads from system environment variables
36///
37/// # Examples
38///
39/// ```rust
40/// use qubit_config::source::{EnvConfigSource, ConfigSource};
41/// use qubit_config::Config;
42///
43/// // Load all env vars
44/// let source = EnvConfigSource::new();
45///
46/// // Load only vars with prefix "APP_", strip prefix and normalize key
47/// let source = EnvConfigSource::with_prefix("APP_");
48///
49/// let mut config = Config::new();
50/// source.load(&mut config).unwrap();
51/// ```
52///
53/// # Author
54///
55/// Haixing Hu
56#[derive(Debug, Clone)]
57pub struct EnvConfigSource {
58    /// Optional prefix filter; only variables with this prefix are loaded
59    prefix: Option<String>,
60    /// Whether to strip the prefix from the key
61    strip_prefix: bool,
62    /// Whether to convert underscores to dots in the key
63    convert_underscores: bool,
64    /// Whether to lowercase the key
65    lowercase_keys: bool,
66}
67
68impl EnvConfigSource {
69    /// Creates a new `EnvConfigSource` that loads all environment variables.
70    ///
71    /// Keys are loaded as-is (no prefix filtering, no transformation).
72    ///
73    /// # Returns
74    ///
75    /// A source that ingests every `std::env::vars()` entry.
76    #[inline]
77    pub fn new() -> Self {
78        Self {
79            prefix: None,
80            strip_prefix: false,
81            convert_underscores: false,
82            lowercase_keys: false,
83        }
84    }
85
86    /// Creates a new `EnvConfigSource` that filters by prefix and normalizes
87    /// keys.
88    ///
89    /// Only variables with the given prefix are loaded. The prefix is stripped,
90    /// the key is lowercased, and underscores are converted to dots.
91    ///
92    /// # Parameters
93    ///
94    /// * `prefix` - The prefix to filter by (e.g., `"APP_"`)
95    ///
96    /// # Returns
97    ///
98    /// A source with prefix filtering and key normalization enabled.
99    #[inline]
100    pub fn with_prefix(prefix: &str) -> Self {
101        Self {
102            prefix: Some(prefix.to_string()),
103            strip_prefix: true,
104            convert_underscores: true,
105            lowercase_keys: true,
106        }
107    }
108
109    /// Creates a new `EnvConfigSource` with a custom prefix and explicit
110    /// options.
111    ///
112    /// # Parameters
113    ///
114    /// * `prefix` - The prefix to filter by
115    /// * `strip_prefix` - Whether to strip the prefix from the key
116    /// * `convert_underscores` - Whether to convert underscores to dots
117    /// * `lowercase_keys` - Whether to lowercase the key
118    ///
119    /// # Returns
120    ///
121    /// A configured [`EnvConfigSource`].
122    #[inline]
123    pub fn with_options(
124        prefix: &str,
125        strip_prefix: bool,
126        convert_underscores: bool,
127        lowercase_keys: bool,
128    ) -> Self {
129        Self {
130            prefix: Some(prefix.to_string()),
131            strip_prefix,
132            convert_underscores,
133            lowercase_keys,
134        }
135    }
136
137    /// Transforms an environment variable key according to the source's
138    /// settings.
139    ///
140    /// # Parameters
141    ///
142    /// * `key` - Original environment variable name.
143    ///
144    /// # Returns
145    ///
146    /// The key after optional prefix strip, lowercasing, and underscore
147    /// replacement.
148    fn transform_key(&self, key: &str) -> String {
149        let mut result = key.to_string();
150
151        if self.strip_prefix
152            && let Some(prefix) = &self.prefix
153            && result.starts_with(prefix.as_str())
154        {
155            result = result[prefix.len()..].to_string();
156        }
157
158        if self.lowercase_keys {
159            result = result.to_lowercase();
160        }
161
162        if self.convert_underscores {
163            result = result.replace('_', ".");
164        }
165
166        result
167    }
168
169    /// Checks whether an environment variable key matches a UTF-8 prefix.
170    ///
171    /// # Parameters
172    ///
173    /// * `key` - Environment variable key from [`std::env::vars_os`].
174    /// * `prefix` - UTF-8 prefix configured on this source.
175    ///
176    /// # Returns
177    ///
178    /// `true` if the key starts with `prefix`. On Unix, non-Unicode keys are
179    /// compared as bytes so unrelated invalid keys can still be skipped by a
180    /// prefixed source.
181    fn env_key_matches_prefix(key: &OsStr, prefix: &str) -> bool {
182        key.to_str().map_or_else(
183            || Self::non_unicode_env_key_matches_prefix(key, prefix),
184            |key| key.starts_with(prefix),
185        )
186    }
187
188    /// Checks a non-Unicode environment key against a UTF-8 prefix.
189    ///
190    /// # Parameters
191    ///
192    /// * `key` - Non-Unicode environment variable key.
193    /// * `prefix` - UTF-8 prefix configured on this source.
194    ///
195    /// # Returns
196    ///
197    /// `true` on Unix when the raw key bytes start with the UTF-8 prefix bytes;
198    /// `false` on platforms where raw environment bytes are unavailable.
199    #[cfg(unix)]
200    fn non_unicode_env_key_matches_prefix(key: &OsStr, prefix: &str) -> bool {
201        use std::os::unix::ffi::OsStrExt;
202
203        key.as_bytes().starts_with(prefix.as_bytes())
204    }
205
206    /// Checks a non-Unicode environment key against a UTF-8 prefix.
207    ///
208    /// # Parameters
209    ///
210    /// * `_key` - Non-Unicode environment variable key.
211    /// * `_prefix` - UTF-8 prefix configured on this source.
212    ///
213    /// # Returns
214    ///
215    /// Always `false` on non-Unix platforms because raw environment bytes are not
216    /// available through the standard library.
217    #[cfg(not(unix))]
218    fn non_unicode_env_key_matches_prefix(_key: &OsStr, _prefix: &str) -> bool {
219        false
220    }
221
222    /// Converts an OS environment string to UTF-8.
223    ///
224    /// # Parameters
225    ///
226    /// * `value` - Environment key or value returned by [`std::env::vars_os`].
227    /// * `label` - Human-readable label included in parse errors.
228    ///
229    /// # Returns
230    ///
231    /// `Ok(String)` when `value` is valid UTF-8.
232    ///
233    /// # Errors
234    ///
235    /// Returns [`ConfigError::ParseError`] when `value` is not valid Unicode,
236    /// preserving a lossy representation in the diagnostic message.
237    fn env_os_string_to_string(value: OsString, label: &str) -> ConfigResult<String> {
238        value.into_string().map_err(|value| {
239            ConfigError::ParseError(format!(
240                "{label} is not valid Unicode: {}",
241                value.to_string_lossy(),
242            ))
243        })
244    }
245}
246
247impl Default for EnvConfigSource {
248    #[inline]
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254impl ConfigSource for EnvConfigSource {
255    fn load(&self, config: &mut Config) -> ConfigResult<()> {
256        for (key_os, value_os) in std::env::vars_os() {
257            // Filter by prefix if set
258            if let Some(prefix) = &self.prefix
259                && !Self::env_key_matches_prefix(&key_os, prefix)
260            {
261                continue;
262            }
263
264            let key = Self::env_os_string_to_string(key_os, "Environment variable key")?;
265            let value = Self::env_os_string_to_string(
266                value_os,
267                &format!("Value for environment variable '{key}'"),
268            )?;
269            let transformed_key = self.transform_key(&key);
270            config.set(&transformed_key, value)?;
271        }
272
273        Ok(())
274    }
275}