1use std::env::{self, VarError};
8use std::str::FromStr;
9
10pub const PREFIX: &str = "WSB_";
12
13#[derive(Debug, thiserror::Error)]
15pub enum EnvError {
16 #[error("environment variable `{key}` is not set")]
18 Missing {
19 key: String,
21 },
22
23 #[error("environment variable `{key}` is set but empty")]
25 Empty {
26 key: String,
28 },
29
30 #[error("environment variable `{key}` is not a valid {expected}: `{value}`")]
32 Invalid {
33 key: String,
35 expected: &'static str,
37 value: String,
39 },
40
41 #[error("environment variable `{key}` is not valid unicode")]
43 NotUnicode {
44 key: String,
46 },
47}
48
49pub fn required(key: &str) -> Result<String, EnvError> {
56 interpret(key, env::var(key))
57}
58
59fn 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#[must_use]
86pub fn optional(key: &str) -> Option<String> {
87 interpret(key, env::var(key)).ok()
88}
89
90pub 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
102fn 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
114pub 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}