init4_bin_base/utils/
from_env.rs

1use std::{env::VarError, num::ParseIntError, str::FromStr};
2
3/// Error type for loading from the environment. See the [`FromEnv`] trait for
4/// more information.
5#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
6pub enum FromEnvErr<Inner> {
7    /// The environment variable is missing.
8    #[error("Error reading variable {0}: {1}")]
9    EnvError(String, VarError),
10    /// The environment variable is empty.
11    #[error("Environment variable {0} is empty")]
12    Empty(String),
13    /// The environment variable is present, but the value could not be parsed.
14    #[error("Failed to parse environment variable {0}")]
15    ParseError(#[from] Inner),
16}
17
18impl<Inner> FromEnvErr<Inner> {
19    /// Missing env var.
20    pub fn env_err(var: &str, e: VarError) -> Self {
21        Self::EnvError(var.to_string(), e)
22    }
23
24    /// Empty env var.
25    pub fn empty(var: &str) -> Self {
26        Self::Empty(var.to_string())
27    }
28
29    /// Error while parsing.
30    pub const fn parse_error(err: Inner) -> Self {
31        Self::ParseError(err)
32    }
33}
34
35/// Convenience function for parsing a value from the environment, if present
36/// and non-empty.
37pub fn parse_env_if_present<T: FromStr>(env_var: &str) -> Result<T, FromEnvErr<T::Err>> {
38    let s = std::env::var(env_var).map_err(|e| FromEnvErr::env_err(env_var, e))?;
39
40    if s.is_empty() {
41        Err(FromEnvErr::empty(env_var))
42    } else {
43        s.parse().map_err(Into::into)
44    }
45}
46
47/// Trait for loading from the environment.
48///
49/// This trait is for structs or other complex objects, that need to be loaded
50/// from the environment. It expects that
51///
52/// - The struct is [`Sized`] and `'static`.
53/// - The struct elements can be parsed from strings.
54/// - Struct elements are at fixed env vars, known by the type at compile time.
55///
56/// As such, unless the env is modified, these are essentially static runtime
57/// values.
58pub trait FromEnv: core::fmt::Debug + Sized + 'static {
59    /// Error type produced when loading from the environment.
60    type Error: core::error::Error;
61
62    /// Load from the environment.
63    fn from_env() -> Result<Self, FromEnvErr<Self::Error>>;
64}
65
66/// Trait for loading primitives from the environment. These are simple types
67/// that should correspond to a single environment variable. It has been
68/// implemented for common integer types, [`String`], [`url::Url`],
69/// [`tracing::Level`], and [`std::time::Duration`].
70///
71/// It aims to make [`FromEnv`] implementations easier to write, by providing a
72/// default implementation for common types.
73pub trait FromEnvVar: core::fmt::Debug + Sized + 'static {
74    /// Error type produced when parsing the primitive.
75    type Error: core::error::Error;
76
77    /// Load the primitive from the environment at the given variable.
78    fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>>;
79}
80
81macro_rules! impl_primitive_from_env {
82    ($($t:ty),*) => {
83        $(
84            impl FromEnvVar for $t {
85                type Error = std::num::ParseIntError;
86
87                fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
88                    parse_env_if_present(env_var)
89                }
90            }
91        )*
92    };
93}
94
95impl_primitive_from_env!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize);
96
97impl FromEnvVar for String {
98    type Error = std::convert::Infallible;
99
100    fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
101        std::env::var(env_var).map_err(|_| FromEnvErr::empty(env_var))
102    }
103}
104
105impl FromEnvVar for url::Url {
106    type Error = url::ParseError;
107
108    fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
109        parse_env_if_present(env_var)
110    }
111}
112
113impl FromEnvVar for tracing::Level {
114    type Error = tracing_core::metadata::ParseLevelError;
115
116    fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
117        parse_env_if_present(env_var)
118    }
119}
120
121impl FromEnvVar for std::time::Duration {
122    type Error = ParseIntError;
123
124    fn from_env_var(s: &str) -> Result<Self, FromEnvErr<Self::Error>> {
125        u64::from_env_var(s).map(Self::from_millis)
126    }
127}
128
129#[cfg(test)]
130mod test {
131    use std::time::Duration;
132
133    use super::*;
134
135    fn set<T>(env: &str, val: &T)
136    where
137        T: ToString,
138    {
139        std::env::set_var(env, val.to_string());
140    }
141
142    fn load_expect_err<T>(env: &str, err: FromEnvErr<T::Error>)
143    where
144        T: FromEnvVar,
145        T::Error: PartialEq,
146    {
147        let res = T::from_env_var(env).unwrap_err();
148        assert_eq!(res, err);
149    }
150
151    fn test<T>(env: &str, val: T)
152    where
153        T: ToString + FromEnvVar + PartialEq + std::fmt::Debug,
154    {
155        set(env, &val);
156
157        let res = T::from_env_var(env).unwrap();
158        assert_eq!(res, val);
159    }
160
161    fn test_expect_err<T, U>(env: &str, value: U, err: FromEnvErr<T::Error>)
162    where
163        T: FromEnvVar,
164        U: ToString,
165        T::Error: PartialEq,
166    {
167        set(env, &value);
168        load_expect_err::<T>(env, err);
169    }
170
171    #[test]
172    fn test_primitives() {
173        test("U8", 42u8);
174        test("U16", 42u16);
175        test("U32", 42u32);
176        test("U64", 42u64);
177        test("U128", 42u128);
178        test("Usize", 42usize);
179        test("I8", 42i8);
180        test("I8-NEG", -42i16);
181        test("I16", 42i16);
182        test("I32", 42i32);
183        test("I64", 42i64);
184        test("I128", 42i128);
185        test("Isize", 42isize);
186        test("String", "hello".to_string());
187        test("Url", url::Url::parse("http://example.com").unwrap());
188        test("Level", tracing::Level::INFO);
189    }
190
191    #[test]
192    fn test_duration() {
193        let amnt = 42;
194        let val = Duration::from_millis(42);
195
196        set("Duration", &amnt);
197        let res = Duration::from_env_var("Duration").unwrap();
198
199        assert_eq!(res, val);
200    }
201
202    #[test]
203    fn test_a_few_errors() {
204        test_expect_err::<u8, _>(
205            "U8_",
206            30000u16,
207            FromEnvErr::parse_error("30000".parse::<u8>().unwrap_err()),
208        );
209
210        test_expect_err::<u8, _>("U8_", "", FromEnvErr::empty("U8_"));
211    }
212}