Skip to main content

webserver_base/
env.rs

1//! Reading configuration out of the environment.
2//!
3//! A variable that is present but blank counts as missing: a deploy whose
4//! secret failed to mount sets an empty string far more often than it unsets
5//! the variable.
6
7use std::env::{self, VarError};
8use std::str::FromStr;
9
10/// The prefix on every environment variable this crate reads.
11pub const PREFIX: &str = "WSB_";
12
13/// Why an environment variable could not be used.
14#[derive(Debug, thiserror::Error)]
15pub enum EnvError {
16    /// The variable is not set at all.
17    #[error("environment variable `{key}` is not set")]
18    Missing {
19        /// The variable's name.
20        key: String,
21    },
22
23    /// The variable is set, but its value is blank once trimmed.
24    #[error("environment variable `{key}` is set but empty")]
25    Empty {
26        /// The variable's name.
27        key: String,
28    },
29
30    /// The variable is set to something that is not valid for its type.
31    #[error("environment variable `{key}` is not a valid {expected}: `{value}`")]
32    Invalid {
33        /// The variable's name.
34        key: String,
35        /// What the value should have been, for the error message.
36        expected: &'static str,
37        /// What it actually was.
38        value: String,
39    },
40
41    /// The variable's value is not valid Unicode.
42    #[error("environment variable `{key}` is not valid unicode")]
43    NotUnicode {
44        /// The variable's name.
45        key: String,
46    },
47}
48
49/// Reads a variable which must be present and non-blank.
50///
51/// # Errors
52///
53/// [`EnvError::Missing`] if unset, [`EnvError::Empty`] if blank once trimmed,
54/// [`EnvError::NotUnicode`] if the value is not valid Unicode.
55pub fn required(key: &str) -> Result<String, EnvError> {
56    interpret(key, env::var(key))
57}
58
59/// [`required`]'s logic over an already-read value, so it can be tested without
60/// mutating the process environment (which is `unsafe`, and forbidden here).
61fn interpret(key: &str, raw: Result<String, VarError>) -> Result<String, EnvError> {
62    match raw {
63        Ok(value) => {
64            let value: String = value.trim().to_string();
65            if value.is_empty() {
66                return Err(EnvError::Empty {
67                    key: key.to_string(),
68                });
69            }
70            Ok(value)
71        }
72        Err(VarError::NotPresent) => Err(EnvError::Missing {
73            key: key.to_string(),
74        }),
75        Err(VarError::NotUnicode(_)) => Err(EnvError::NotUnicode {
76            key: key.to_string(),
77        }),
78    }
79}
80
81/// Reads a variable which may be absent.
82///
83/// A blank value is reported as absent, for the reason given in the module
84/// docs.
85#[must_use]
86pub fn optional(key: &str) -> Option<String> {
87    interpret(key, env::var(key)).ok()
88}
89
90/// Reads and parses a variable which must be present and non-blank.
91///
92/// # Errors
93///
94/// As [`required`], plus [`EnvError::Invalid`] if the value does not parse.
95pub fn parse_required<T>(key: &str, expected: &'static str) -> Result<T, EnvError>
96where
97    T: FromStr,
98{
99    parse_value(key, expected, required(key)?)
100}
101
102/// Parses an already-read value, naming it in any failure.
103fn parse_value<T>(key: &str, expected: &'static str, value: String) -> Result<T, EnvError>
104where
105    T: FromStr,
106{
107    value.parse::<T>().map_err(|_| EnvError::Invalid {
108        key: key.to_string(),
109        expected,
110        value,
111    })
112}
113
114/// Reads and parses a variable, falling back to `default` when absent.
115///
116/// # Errors
117///
118/// [`EnvError::Invalid`] if present but malformed — substituting the default
119/// for a typo is how a server ends up listening on the wrong port.
120pub fn parse_or<T>(key: &str, expected: &'static str, default: T) -> Result<T, EnvError>
121where
122    T: FromStr,
123{
124    match optional(key) {
125        None => Ok(default),
126        Some(value) => parse_value(key, expected, value),
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use std::env::VarError;
133
134    use super::{EnvError, interpret, optional, parse_or, parse_value, required};
135
136    #[test]
137    fn a_present_value_is_trimmed() {
138        let expected: String = String::from("hello");
139        let actual: String = interpret("WSB_X", Ok(String::from("  hello  "))).expect("present");
140        assert_eq!(expected, actual);
141    }
142
143    #[test]
144    fn a_missing_variable_names_itself() {
145        let error: EnvError =
146            interpret("WSB_X", Err(VarError::NotPresent)).expect_err("not present");
147        assert!(matches!(error, EnvError::Missing { ref key } if key == "WSB_X"));
148
149        let expected: String = String::from("environment variable `WSB_X` is not set");
150        let actual: String = error.to_string();
151        assert_eq!(expected, actual);
152    }
153
154    #[test]
155    fn a_blank_variable_is_reported_distinctly_from_a_missing_one() {
156        let error: EnvError = interpret("WSB_X", Ok(String::from("   "))).expect_err("blank");
157        assert!(matches!(error, EnvError::Empty { ref key } if key == "WSB_X"));
158
159        let expected: String = String::from("environment variable `WSB_X` is set but empty");
160        let actual: String = error.to_string();
161        assert_eq!(expected, actual);
162    }
163
164    #[test]
165    fn a_non_unicode_value_is_its_own_failure() {
166        let error: EnvError = interpret(
167            "WSB_X",
168            Err(VarError::NotUnicode(std::ffi::OsString::new())),
169        )
170        .expect_err("not unicode");
171        assert!(matches!(error, EnvError::NotUnicode { ref key } if key == "WSB_X"));
172    }
173
174    #[test]
175    fn parsing_reports_the_offending_value() {
176        let error: EnvError =
177            parse_value::<u16>("WSB_PORT", "port number", String::from("not-a-number"))
178                .expect_err("not a u16");
179
180        let expected: String = String::from(
181            "environment variable `WSB_PORT` is not a valid port number: `not-a-number`",
182        );
183        let actual: String = error.to_string();
184        assert_eq!(expected, actual);
185    }
186
187    #[test]
188    fn parsing_succeeds_for_a_well_formed_value() {
189        let expected: u16 = 8080;
190        let actual: u16 =
191            parse_value("WSB_PORT", "port number", String::from("8080")).expect("valid");
192        assert_eq!(expected, actual);
193    }
194
195    #[test]
196    fn an_absent_variable_falls_back_to_the_default() {
197        let expected: u16 = 8080;
198        let actual: u16 = parse_or("WSB_DEFINITELY_NOT_SET_ANYWHERE", "port number", 8080)
199            .expect("absent is fine");
200        assert_eq!(expected, actual);
201    }
202
203    #[test]
204    fn the_public_wrappers_agree_with_the_logic_they_wrap() {
205        let error: EnvError = required("WSB_DEFINITELY_NOT_SET_ANYWHERE").expect_err("absent");
206        assert!(matches!(error, EnvError::Missing { .. }));
207
208        let expected: Option<String> = None;
209        let actual: Option<String> = optional("WSB_DEFINITELY_NOT_SET_ANYWHERE");
210        assert_eq!(expected, actual);
211    }
212}