Skip to main content

qubit_config/source/
env_config_source.rs

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