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}