1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
67#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
68#![allow(clippy::multiple_crate_versions)]
69
70use std::collections::BTreeMap;
71
72#[cfg(feature = "std")]
77pub mod standard;
78
79#[cfg(feature = "simulator")]
84pub mod simulator;
85
86#[derive(Debug, thiserror::Error)]
90pub enum EnvError {
91 #[error("Environment variable '{0}' not found")]
93 NotFound(String),
94 #[error("Environment variable '{0}' has invalid value: {1}")]
96 InvalidValue(String, String),
97 #[error("Parse error for '{0}': {1}")]
99 ParseError(String, String),
100}
101
102pub type Result<T> = std::result::Result<T, EnvError>;
106
107pub trait EnvProvider: Send + Sync {
138 fn var(&self, name: &str) -> Result<String>;
144
145 #[must_use]
147 fn var_or(&self, name: &str, default: &str) -> String {
148 self.var(name).unwrap_or_else(|_| default.to_string())
149 }
150
151 fn var_parse<T>(&self, name: &str) -> Result<T>
158 where
159 T: std::str::FromStr,
160 T::Err: std::fmt::Display,
161 {
162 let value = self.var(name)?;
163 value
164 .parse::<T>()
165 .map_err(|e| EnvError::ParseError(name.to_string(), e.to_string()))
166 }
167
168 #[must_use]
170 fn var_parse_or<T>(&self, name: &str, default: T) -> T
171 where
172 T: std::str::FromStr,
173 T::Err: std::fmt::Display,
174 {
175 self.var_parse(name).unwrap_or(default)
176 }
177
178 fn var_parse_opt<T>(&self, name: &str) -> Result<Option<T>>
190 where
191 T: std::str::FromStr,
192 T::Err: std::fmt::Display,
193 {
194 match self.var(name) {
195 Ok(value) => value
196 .parse::<T>()
197 .map(Some)
198 .map_err(|e| EnvError::ParseError(name.to_string(), e.to_string())),
199 Err(EnvError::NotFound(_)) => Ok(None),
200 Err(e) => Err(e),
201 }
202 }
203
204 #[must_use]
206 fn var_exists(&self, name: &str) -> bool {
207 self.var(name).is_ok()
208 }
209
210 #[must_use]
212 fn vars(&self) -> BTreeMap<String, String>;
213}
214
215#[allow(unused)]
216macro_rules! impl_env {
217 ($module:ident $(,)?) => {
218 pub use $module::{var, var_exists, var_or, var_parse, var_parse_opt, var_parse_or, vars};
219 };
220}
221
222#[cfg(feature = "simulator")]
223impl_env!(simulator);
224
225#[cfg(all(not(feature = "simulator"), feature = "std"))]
226impl_env!(standard);
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 struct TestEnvProvider {
233 vars: BTreeMap<String, String>,
234 }
235
236 impl EnvProvider for TestEnvProvider {
237 fn var(&self, name: &str) -> Result<String> {
238 self.vars
239 .get(name)
240 .cloned()
241 .ok_or_else(|| EnvError::NotFound(name.to_string()))
242 }
243
244 fn vars(&self) -> BTreeMap<String, String> {
245 self.vars.clone()
246 }
247 }
248
249 #[test_log::test]
250 fn test_custom_env_provider_implementation() {
251 let mut vars = BTreeMap::new();
252 vars.insert("KEY1".to_string(), "value1".to_string());
253 vars.insert("KEY2".to_string(), "value2".to_string());
254
255 let provider = TestEnvProvider { vars };
256
257 assert_eq!(provider.var("KEY1").unwrap(), "value1");
258 assert_eq!(provider.var("KEY2").unwrap(), "value2");
259 assert!(matches!(provider.var("KEY3"), Err(EnvError::NotFound(_))));
260 }
261
262 #[test_log::test]
263 fn test_env_provider_var_or() {
264 let provider = TestEnvProvider {
265 vars: BTreeMap::new(),
266 };
267
268 assert_eq!(provider.var_or("MISSING", "default"), "default");
269 }
270
271 #[test_log::test]
272 fn test_env_provider_var_or_with_existing() {
273 let mut vars = BTreeMap::new();
274 vars.insert("EXISTS".to_string(), "actual".to_string());
275
276 let provider = TestEnvProvider { vars };
277
278 assert_eq!(provider.var_or("EXISTS", "default"), "actual");
279 }
280
281 #[test_log::test]
282 fn test_env_provider_var_parse_success() {
283 let mut vars = BTreeMap::new();
284 vars.insert("NUMBER".to_string(), "42".to_string());
285
286 let provider = TestEnvProvider { vars };
287
288 let result: i32 = provider.var_parse("NUMBER").unwrap();
289 assert_eq!(result, 42);
290 }
291
292 #[test_log::test]
293 fn test_env_provider_var_parse_error() {
294 let mut vars = BTreeMap::new();
295 vars.insert("NOT_NUMBER".to_string(), "abc".to_string());
296
297 let provider = TestEnvProvider { vars };
298
299 let result: Result<i32> = provider.var_parse("NOT_NUMBER");
300 assert!(matches!(result, Err(EnvError::ParseError(_, _))));
301 }
302
303 #[test_log::test]
304 fn test_env_provider_var_parse_missing() {
305 let provider = TestEnvProvider {
306 vars: BTreeMap::new(),
307 };
308
309 let result: Result<i32> = provider.var_parse("MISSING");
310 assert!(matches!(result, Err(EnvError::NotFound(_))));
311 }
312
313 #[test_log::test]
314 fn test_env_provider_var_parse_or_success() {
315 let mut vars = BTreeMap::new();
316 vars.insert("NUMBER".to_string(), "100".to_string());
317
318 let provider = TestEnvProvider { vars };
319
320 let result: i32 = provider.var_parse_or("NUMBER", 42);
321 assert_eq!(result, 100);
322 }
323
324 #[test_log::test]
325 fn test_env_provider_var_parse_or_with_parse_error() {
326 let mut vars = BTreeMap::new();
327 vars.insert("NOT_NUMBER".to_string(), "xyz".to_string());
328
329 let provider = TestEnvProvider { vars };
330
331 let result: i32 = provider.var_parse_or("NOT_NUMBER", 42);
332 assert_eq!(result, 42);
333 }
334
335 #[test_log::test]
336 fn test_env_provider_var_parse_or_with_missing() {
337 let provider = TestEnvProvider {
338 vars: BTreeMap::new(),
339 };
340
341 let result: i32 = provider.var_parse_or("MISSING", 42);
342 assert_eq!(result, 42);
343 }
344
345 #[test_log::test]
346 fn test_env_provider_var_parse_opt_some() {
347 let mut vars = BTreeMap::new();
348 vars.insert("NUMBER".to_string(), "123".to_string());
349
350 let provider = TestEnvProvider { vars };
351
352 let result: Option<i32> = provider.var_parse_opt("NUMBER").unwrap();
353 assert_eq!(result, Some(123));
354 }
355
356 #[test_log::test]
357 fn test_env_provider_var_parse_opt_none() {
358 let provider = TestEnvProvider {
359 vars: BTreeMap::new(),
360 };
361
362 let result: Option<i32> = provider.var_parse_opt("MISSING").unwrap();
363 assert_eq!(result, None);
364 }
365
366 #[test_log::test]
367 fn test_env_provider_var_parse_opt_parse_error() {
368 let mut vars = BTreeMap::new();
369 vars.insert("NOT_NUMBER".to_string(), "not_an_int".to_string());
370
371 let provider = TestEnvProvider { vars };
372
373 let result: Result<Option<i32>> = provider.var_parse_opt("NOT_NUMBER");
374 assert!(matches!(result, Err(EnvError::ParseError(_, _))));
375 }
376
377 #[test_log::test]
378 fn test_env_provider_var_exists_true() {
379 let mut vars = BTreeMap::new();
380 vars.insert("EXISTS".to_string(), "yes".to_string());
381
382 let provider = TestEnvProvider { vars };
383
384 assert!(provider.var_exists("EXISTS"));
385 }
386
387 #[test_log::test]
388 fn test_env_provider_var_exists_false() {
389 let provider = TestEnvProvider {
390 vars: BTreeMap::new(),
391 };
392
393 assert!(!provider.var_exists("MISSING"));
394 }
395
396 #[test_log::test]
397 fn test_env_provider_vars() {
398 let mut vars = BTreeMap::new();
399 vars.insert("KEY1".to_string(), "value1".to_string());
400 vars.insert("KEY2".to_string(), "value2".to_string());
401
402 let provider = TestEnvProvider { vars: vars.clone() };
403
404 let result = provider.vars();
405 assert_eq!(result, vars);
406 }
407
408 #[test_log::test]
409 fn test_result_type_alias() {
410 let ok_result: Result<String> = Ok("test".to_string());
411 assert!(ok_result.is_ok());
412
413 let err_result: Result<String> = Err(EnvError::NotFound("VAR".to_string()));
414 assert!(err_result.is_err());
415 }
416
417 struct InvalidValueProvider {
419 invalid_vars: std::collections::BTreeSet<String>,
420 }
421
422 impl EnvProvider for InvalidValueProvider {
423 fn var(&self, name: &str) -> Result<String> {
424 if self.invalid_vars.contains(name) {
425 Err(EnvError::InvalidValue(
426 name.to_string(),
427 "contains null byte".to_string(),
428 ))
429 } else {
430 Err(EnvError::NotFound(name.to_string()))
431 }
432 }
433
434 fn vars(&self) -> BTreeMap<String, String> {
435 BTreeMap::new()
436 }
437 }
438
439 #[test_log::test]
440 fn test_env_provider_var_parse_opt_propagates_invalid_value_error() {
441 let mut invalid_vars = std::collections::BTreeSet::new();
444 invalid_vars.insert("INVALID_VAR".to_string());
445
446 let provider = InvalidValueProvider { invalid_vars };
447
448 let result: Result<Option<i32>> = provider.var_parse_opt("INVALID_VAR");
449
450 assert!(matches!(
452 result,
453 Err(EnvError::InvalidValue(ref name, _)) if name == "INVALID_VAR"
454 ));
455 }
456
457 #[test_log::test]
458 fn test_env_provider_var_parse_propagates_invalid_value_error() {
459 let mut invalid_vars = std::collections::BTreeSet::new();
461 invalid_vars.insert("INVALID_VAR".to_string());
462
463 let provider = InvalidValueProvider { invalid_vars };
464
465 let result: Result<i32> = provider.var_parse("INVALID_VAR");
466
467 assert!(matches!(
469 result,
470 Err(EnvError::InvalidValue(ref name, _)) if name == "INVALID_VAR"
471 ));
472 }
473}