Skip to main content

everruns_core/config/
env.rs

1// Environment variable loading helpers
2//
3// Each function encapsulates the read → parse → fallback pattern that is
4// repeated dozens of times across config structs.
5
6use std::str::FromStr;
7use std::time::Duration;
8
9use super::ConfigError;
10
11/// Read an env var, parse it to `T`, or return `default`.
12///
13/// Silently falls back on missing or unparseable values — use [`env_required`]
14/// when the caller needs to distinguish those cases.
15pub fn env_or<T: FromStr>(var: &str, default: T) -> T {
16    std::env::var(var)
17        .ok()
18        .and_then(|v| v.parse().ok())
19        .unwrap_or(default)
20}
21
22/// Read an env var as a `String`, or return `default`.
23pub fn env_string(var: &str, default: &str) -> String {
24    std::env::var(var).unwrap_or_else(|_| default.to_string())
25}
26
27/// Read the first non-empty env var from `vars`, or return `default`.
28pub fn env_string_any(vars: &[&str], default: &str) -> String {
29    vars.iter()
30        .find_map(|var| env_opt_string(var))
31        .unwrap_or_else(|| default.to_string())
32}
33
34/// Read an env var as a `bool` (`"true"` or `"1"`), or return `default`.
35pub fn env_bool(var: &str, default: bool) -> bool {
36    std::env::var(var)
37        .ok()
38        .map(|v| v == "true" || v == "1")
39        .unwrap_or(default)
40}
41
42/// Read an env var as a `Duration` in whole seconds, or return `default`.
43pub fn env_duration_secs(var: &str, default: Duration) -> Duration {
44    std::env::var(var)
45        .ok()
46        .and_then(|v| v.parse::<u64>().ok())
47        .map(Duration::from_secs)
48        .unwrap_or(default)
49}
50
51/// Read an env var as a `Duration` in milliseconds, or return `default`.
52pub fn env_duration_ms(var: &str, default: Duration) -> Duration {
53    std::env::var(var)
54        .ok()
55        .and_then(|v| v.parse::<u64>().ok())
56        .map(Duration::from_millis)
57        .unwrap_or(default)
58}
59
60/// Read an optional env var, parsing it to `T`. Returns `None` if the var is
61/// unset or empty.
62pub fn env_opt<T: FromStr>(var: &str) -> Option<T> {
63    std::env::var(var)
64        .ok()
65        .filter(|s| !s.is_empty())
66        .and_then(|v| v.parse().ok())
67}
68
69/// Read a required env var, parsing it to `T`. Returns `Err` if unset or
70/// unparseable.
71pub fn env_required<T: FromStr>(var: &str) -> Result<T, ConfigError> {
72    let value = std::env::var(var).map_err(|_| ConfigError::Missing {
73        var: var.to_string(),
74    })?;
75    value.parse().map_err(|_| ConfigError::Invalid {
76        var: var.to_string(),
77        value: value.clone(),
78        reason: format!("could not parse as {}", std::any::type_name::<T>()),
79    })
80}
81
82/// Read an env var as a comma-separated list of `T`. Returns an empty vec if
83/// the var is unset or empty. Items that fail to parse are silently skipped.
84pub fn env_list<T: FromStr>(var: &str) -> Vec<T> {
85    std::env::var(var)
86        .ok()
87        .filter(|s| !s.is_empty())
88        .map(|s| {
89            s.split(',')
90                .filter_map(|item| item.trim().parse().ok())
91                .collect()
92        })
93        .unwrap_or_default()
94}
95
96/// Read an env var as a `String`, or return `None` if unset/empty.
97pub fn env_opt_string(var: &str) -> Option<String> {
98    std::env::var(var).ok().filter(|s| !s.is_empty())
99}
100
101/// Read the first non-empty env var from `vars`.
102pub fn env_opt_string_any(vars: &[&str]) -> Option<String> {
103    vars.iter().find_map(|var| env_opt_string(var))
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use std::env;
110
111    // Use a prefix to avoid collisions with real env vars in test runners
112    const PREFIX: &str = "EVERRUNS_CONFIG_TEST_";
113
114    fn set(suffix: &str, val: &str) -> String {
115        let key = format!("{PREFIX}{suffix}");
116        // SAFETY: Tests run single-threaded (cargo test -- --test-threads=1) or
117        // use unique key prefixes so concurrent mutation is safe.
118        unsafe { env::set_var(&key, val) };
119        key
120    }
121
122    fn unset(suffix: &str) -> String {
123        let key = format!("{PREFIX}{suffix}");
124        // SAFETY: see `set` above.
125        unsafe { env::remove_var(&key) };
126        key
127    }
128
129    #[test]
130    fn env_or_present() {
131        let key = set("OR_PRESENT", "42");
132        assert_eq!(env_or::<u32>(&key, 0), 42);
133    }
134
135    #[test]
136    fn env_or_missing() {
137        let key = unset("OR_MISSING");
138        assert_eq!(env_or::<u32>(&key, 7), 7);
139    }
140
141    #[test]
142    fn env_or_unparseable() {
143        let key = set("OR_BAD", "not_a_number");
144        assert_eq!(env_or::<u32>(&key, 99), 99);
145    }
146
147    #[test]
148    fn env_string_present() {
149        let key = set("STR_P", "hello");
150        assert_eq!(env_string(&key, "default"), "hello");
151    }
152
153    #[test]
154    fn env_string_missing() {
155        let key = unset("STR_M");
156        assert_eq!(env_string(&key, "fallback"), "fallback");
157    }
158
159    #[test]
160    fn env_string_any_uses_first_non_empty_value() {
161        let first = unset("STR_ANY_FIRST");
162        let second = set("STR_ANY_SECOND", "");
163        let third = set("STR_ANY_THIRD", "value");
164
165        assert_eq!(
166            env_string_any(&[&first, &second, &third], "fallback"),
167            "value"
168        );
169    }
170
171    #[test]
172    fn env_string_any_falls_back_to_default() {
173        let first = unset("STR_ANY_MISSING_FIRST");
174        let second = set("STR_ANY_MISSING_SECOND", "");
175
176        assert_eq!(env_string_any(&[&first, &second], "fallback"), "fallback");
177    }
178
179    #[test]
180    fn env_bool_true_variants() {
181        let key = set("BOOL_T", "true");
182        assert!(env_bool(&key, false));
183        let key = set("BOOL_1", "1");
184        assert!(env_bool(&key, false));
185    }
186
187    #[test]
188    fn env_bool_false() {
189        let key = set("BOOL_F", "false");
190        assert!(!env_bool(&key, true));
191    }
192
193    #[test]
194    fn env_bool_missing() {
195        let key = unset("BOOL_M");
196        assert!(env_bool(&key, true));
197    }
198
199    #[test]
200    fn env_duration_secs_present() {
201        let key = set("DUR_S", "10");
202        assert_eq!(
203            env_duration_secs(&key, Duration::from_secs(1)),
204            Duration::from_secs(10)
205        );
206    }
207
208    #[test]
209    fn env_duration_secs_missing() {
210        let key = unset("DUR_S_M");
211        assert_eq!(
212            env_duration_secs(&key, Duration::from_secs(5)),
213            Duration::from_secs(5)
214        );
215    }
216
217    #[test]
218    fn env_duration_ms_present() {
219        let key = set("DUR_MS", "250");
220        assert_eq!(
221            env_duration_ms(&key, Duration::from_millis(100)),
222            Duration::from_millis(250)
223        );
224    }
225
226    #[test]
227    fn env_opt_present() {
228        let key = set("OPT_P", "42");
229        assert_eq!(env_opt::<u32>(&key), Some(42));
230    }
231
232    #[test]
233    fn env_opt_missing() {
234        let key = unset("OPT_M");
235        assert_eq!(env_opt::<u32>(&key), None::<u32>);
236    }
237
238    #[test]
239    fn env_opt_empty() {
240        let key = set("OPT_E", "");
241        assert_eq!(env_opt::<u32>(&key), None::<u32>);
242    }
243
244    #[test]
245    fn env_required_ok() {
246        let key = set("REQ_OK", "100");
247        assert_eq!(env_required::<u32>(&key).unwrap(), 100);
248    }
249
250    #[test]
251    fn env_required_missing() {
252        let key = unset("REQ_M");
253        let err = env_required::<u32>(&key).unwrap_err();
254        assert!(matches!(err, ConfigError::Missing { .. }));
255    }
256
257    #[test]
258    fn env_required_invalid() {
259        let key = set("REQ_BAD", "xyz");
260        let err = env_required::<u32>(&key).unwrap_err();
261        assert!(matches!(err, ConfigError::Invalid { .. }));
262    }
263
264    #[test]
265    fn env_list_present() {
266        let key = set("LIST_P", "1, 2, 3");
267        assert_eq!(env_list::<u32>(&key), vec![1, 2, 3]);
268    }
269
270    #[test]
271    fn env_list_missing() {
272        let key = unset("LIST_M");
273        assert_eq!(env_list::<u32>(&key), Vec::<u32>::new());
274    }
275
276    #[test]
277    fn env_list_with_bad_items() {
278        let key = set("LIST_B", "1,bad,3");
279        assert_eq!(env_list::<u32>(&key), vec![1, 3]);
280    }
281
282    #[test]
283    fn env_opt_string_present() {
284        let key = set("OSTR_P", "value");
285        assert_eq!(env_opt_string(&key), Some("value".to_string()));
286    }
287
288    #[test]
289    fn env_opt_string_empty() {
290        let key = set("OSTR_E", "");
291        assert_eq!(env_opt_string(&key), None);
292    }
293
294    #[test]
295    fn env_opt_string_missing() {
296        let key = unset("OSTR_M");
297        assert_eq!(env_opt_string(&key), None);
298    }
299
300    #[test]
301    fn env_opt_string_any_uses_first_non_empty_value() {
302        let first = unset("OSTR_ANY_FIRST");
303        let second = set("OSTR_ANY_SECOND", "");
304        let third = set("OSTR_ANY_THIRD", "value");
305
306        assert_eq!(
307            env_opt_string_any(&[&first, &second, &third]),
308            Some("value".to_string())
309        );
310    }
311}