1pub mod prelude {
53 pub use super::{
54 get_env,
55 get_env_or,
56 get_env_or_default,
57 get_env_or_panic,
58 get_secret,
59 };
60}
61
62#[must_use]
64fn get_env_internal(name: &str) -> Option<String> {
65 match std::env::var(name) {
66 Ok(value) => Some(value),
67 Err(std::env::VarError::NotPresent) => None,
68 Err(std::env::VarError::NotUnicode(_)) => {
69 tracing::error!("Failed to read environment variable `{name}`: Not Unicode.");
70 None
71 },
72 }
73}
74
75fn read_secret_file(path: &str) -> std::io::Result<String> {
81 let content = std::fs::read_to_string(path)?;
82 Ok(content.trim().to_string())
83}
84
85#[must_use = "Ignoring the result may cause unexpected behavior due to missing or invalid configuration."]
96pub fn get_env<T: std::str::FromStr>(
97 name: &str,
98 has_secret: bool,
99) -> Option<T> {
100 if has_secret {
101 let mut secret_name = name.to_owned();
103 secret_name.push_str("_FILE");
104
105 if let Some(value) = get_secret(&secret_name) {
106 return Some(value);
107 }
108 }
109
110 let value = get_env_internal(name)?;
111 match value.parse() {
112 Ok(parsed_value) => Some(parsed_value),
113 Err(_) => {
114 tracing::error!("Failed to parse environment variable `{name}`.");
115 None
116 },
117 }
118}
119
120#[must_use = "Secrets should be handled explicitly; ignoring the result may lead to misconfiguration."]
122pub fn get_secret<T: std::str::FromStr>(
123 name: &str,
124) -> Option<T> {
125 let path = get_env_internal(name)?;
126 match read_secret_file(&path) {
127 Ok(value) => {
128 match value.parse() {
129 Ok(parsed_value) => Some(parsed_value),
130 Err(_) => {
131 tracing::error!("Failed to parse Docker secret `{name}`.");
132 None
133 },
134 }
135 },
136 Err(error) => {
137 tracing::error!("Failed to read Docker secret: {error}");
138 None
139 },
140 }
141}
142
143#[must_use = "Ignoring the result may hide misconfigured or missing environment variables."]
148pub fn get_env_or<T: std::str::FromStr>(
149 name: &str,
150 default: T,
151 has_secret: bool,
152) -> T {
153 get_env(name, has_secret).unwrap_or(default)
154}
155
156#[must_use = "Ignoring the result may hide misconfigured or missing environment variables."]
161pub fn get_env_or_default<T: std::str::FromStr + Default>(
162 name: &str,
163 has_secret: bool,
164) -> T {
165 get_env(name, has_secret).unwrap_or_default()
166}
167
168#[must_use = "This function panics if the variable is missing; ignoring the result defeats its purpose."]
176pub fn get_env_or_panic<T: std::str::FromStr>(
177 name: &str,
178 has_secret: bool,
179) -> T {
180 get_env(name, has_secret).unwrap_or_else(|| {
181 panic!("Environment variable `{name}` is required but not set.");
182 })
183}
184
185#[cfg(test)]
186mod tests {
187 use std::{
188 env,
189 io::Write,
190 };
191 use serial_test::serial;
192 use tempfile::NamedTempFile;
193 use super::*;
194
195 #[test]
196 #[serial]
197 fn test_get_env_internal() {
198 unsafe {
200 env::set_var("TEST_ENV", "test_value");
201 }
202 assert_eq!(get_env_internal("TEST_ENV"), Some("test_value".to_string()));
203
204 unsafe {
206 env::remove_var("TEST_ENV");
207 }
208 assert_eq!(get_env_internal("TEST_ENV"), None);
209 }
210
211 #[cfg(target_family = "unix")]
212 #[test]
213 #[serial]
214 fn test_get_env_internal_invalid_unicode_linux() {
215 use std::ffi::OsString;
216 use std::os::unix::ffi::OsStringExt;
217
218 let invalid_utf8 = OsString::from_vec(vec![0xff, 0xfe, 0xfd]);
220 unsafe {
222 std::env::set_var("INVALID_UNICODE_ENV", &invalid_utf8);
223 }
224
225 assert_eq!(get_env_internal("INVALID_UNICODE_ENV"), None);
227
228 unsafe {
231 std::env::remove_var("INVALID_UNICODE_ENV");
232 }
233 }
234
235 #[test]
236 #[serial]
237 fn test_read_secret_file() {
238 let mut temp_file = NamedTempFile::new().unwrap();
239 writeln!(temp_file, "secret_value").unwrap();
240
241 let path = temp_file.path().to_str().unwrap();
242 let result = read_secret_file(path).unwrap();
243
244 assert_eq!(result, "secret_value");
245
246 let invalid_path = "/invalid/path/to/secret";
248 assert!(read_secret_file(invalid_path).is_err());
249 }
250
251 #[test]
252 #[serial]
253 fn test_get_env() {
254 let mut temp_file = NamedTempFile::new().unwrap();
256 writeln!(temp_file, "docker_secret_value").unwrap();
257
258 let path = temp_file.path().to_str().unwrap();
260 unsafe {
262 env::set_var("TEST_ENV_FILE", path); env::set_var("TEST_ENV", "env_value"); }
265
266 assert_eq!(get_env("TEST_ENV", true), Some("docker_secret_value".to_string()));
268
269 assert_eq!(get_env("TEST_ENV", false), Some("env_value".to_string()));
271
272 unsafe {
275 env::remove_var("TEST_ENV_FILE");
276 env::remove_var("TEST_ENV");
277 }
278 }
279
280 #[test]
281 #[serial]
282 fn test_get_env_or_panic() {
283 let mut temp_file = NamedTempFile::new().unwrap();
284 writeln!(temp_file, "required_secret_value").unwrap();
285
286 let path = temp_file.path().to_str().unwrap();
287 unsafe {
289 env::set_var("REQUIRED_ENV_FILE", path);
290 }
291
292 assert_eq!(get_env_or_panic::<String>("REQUIRED_ENV", true), "required_secret_value".to_string());
294
295 unsafe {
297 env::set_var("REQUIRED_ENV", "required_env_value");
298 }
299
300 assert_eq!(get_env_or_panic::<String>("REQUIRED_ENV", false), "required_env_value".to_string());
302
303 unsafe {
306 env::remove_var("REQUIRED_ENV_FILE");
307 env::remove_var("REQUIRED_ENV");
308 }
309
310 let result = std::panic::catch_unwind(|| {
311 _ = get_env_or_panic::<String>("REQUIRED_ENV", true);
312 });
313 assert!(result.is_err());
314 }
315
316 #[test]
317 #[serial]
318 fn test_get_env_or_variants() {
319 unsafe {
321 env::remove_var("OPTIONAL_ENV");
322 }
323
324 assert_eq!(get_env_or("OPTIONAL_ENV", 123u32, false), 123);
325 assert_eq!(get_env_or_default::<u32>("OPTIONAL_ENV", false), 0);
326
327 unsafe {
329 env::set_var("OPTIONAL_ENV", "456");
330 }
331
332 assert_eq!(get_env_or("OPTIONAL_ENV", 123u32, false), 456);
333 assert_eq!(get_env_or_default::<u32>("OPTIONAL_ENV", false), 456);
334 }
335}