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}