init4_bin_base/utils/
from_env.rs1use std::{env::VarError, num::ParseIntError, str::FromStr};
2
3#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
6pub enum FromEnvErr<Inner> {
7 #[error("Error reading variable {0}: {1}")]
9 EnvError(String, VarError),
10 #[error("Environment variable {0} is empty")]
12 Empty(String),
13 #[error("Failed to parse environment variable {0}")]
15 ParseError(#[from] Inner),
16}
17
18impl<Inner> FromEnvErr<Inner> {
19 pub fn env_err(var: &str, e: VarError) -> Self {
21 Self::EnvError(var.to_string(), e)
22 }
23
24 pub fn empty(var: &str) -> Self {
26 Self::Empty(var.to_string())
27 }
28
29 pub const fn parse_error(err: Inner) -> Self {
31 Self::ParseError(err)
32 }
33}
34
35pub 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
47pub trait FromEnv: core::fmt::Debug + Sized + 'static {
59 type Error: core::error::Error;
61
62 fn from_env() -> Result<Self, FromEnvErr<Self::Error>>;
64}
65
66pub trait FromEnvVar: core::fmt::Debug + Sized + 'static {
74 type Error: core::error::Error;
76
77 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}