Skip to main content

yt/
env.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4/// The process environment, read once at startup.
5#[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    /// The value of `key`; an empty value counts as unset.
35    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    /// An absolute path from `key`; a relative path is ignored.
52    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}