1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4#[derive(Clone, Debug, Default)]
6pub struct Env {
7 vars: BTreeMap<String, String>,
8}
9
10impl Env {
11 #[allow(
12 clippy::disallowed_methods,
13 reason = "the one place that reads the process environment"
14 )]
15 pub fn from_process() -> Self {
16 Self::from_vars(
17 std::env::vars_os().filter_map(|(key, value)| {
18 Some((key.into_string().ok()?, value.into_string().ok()?))
19 }),
20 )
21 }
22
23 pub fn from_vars<K: Into<String>, V: Into<String>>(
24 vars: impl IntoIterator<Item = (K, V)>,
25 ) -> Self {
26 Self {
27 vars: vars
28 .into_iter()
29 .map(|(k, v)| (k.into(), v.into()))
30 .collect(),
31 }
32 }
33
34 pub fn get(&self, key: &str) -> Option<&str> {
36 self.vars
37 .get(key)
38 .map(String::as_str)
39 .filter(|value| !value.is_empty())
40 }
41
42 pub fn is_true(&self, key: &str) -> bool {
43 self.get(key).is_some_and(|value| {
44 matches!(
45 value.to_ascii_lowercase().as_str(),
46 "1" | "true" | "yes" | "y" | "on"
47 )
48 })
49 }
50
51 pub fn path(&self, key: &str) -> Option<PathBuf> {
53 self.get(key)
54 .map(PathBuf::from)
55 .filter(|path| path.is_absolute())
56 }
57
58 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
59 self.vars
60 .iter()
61 .map(|(key, value)| (key.as_str(), value.as_str()))
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn empty_values_count_as_unset() {
71 let env = Env::from_vars([("A", ""), ("B", "x")]);
72 assert_eq!(env.get("A"), None);
73 assert_eq!(env.get("B"), Some("x"));
74 assert_eq!(env.get("C"), None);
75 }
76
77 #[test]
78 fn truthy_values() {
79 let env = Env::from_vars([
80 ("A", "1"),
81 ("B", "Yes"),
82 ("C", "on"),
83 ("D", "0"),
84 ("E", "false"),
85 ]);
86 assert!(env.is_true("A") && env.is_true("B") && env.is_true("C"));
87 assert!(!env.is_true("D") && !env.is_true("E") && !env.is_true("MISSING"));
88 }
89
90 #[test]
91 fn relative_paths_are_ignored() {
92 let env = Env::from_vars([("ABS", "/tmp/x"), ("REL", "tmp/x")]);
93 assert_eq!(env.path("ABS"), Some(PathBuf::from("/tmp/x")));
94 assert_eq!(env.path("REL"), None);
95 }
96}