1use std::{
2 fmt,
3 net::{IpAddr, SocketAddr},
4 time::Duration,
5};
6
7use anyhow::{Context, Result, anyhow, ensure};
8
9type Lookup<'a> = dyn Fn(&str) -> Option<String> + 'a;
10
11#[derive(Clone, Copy)]
12pub(super) struct EnvKey(&'static str);
13
14impl EnvKey {
15 fn new(key: &'static str) -> Self {
16 Self(key)
17 }
18
19 fn as_str(self) -> &'static str {
20 self.0
21 }
22}
23
24impl fmt::Display for EnvKey {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 f.write_str(self.0)
27 }
28}
29
30pub(super) struct EnvValue {
31 key: EnvKey,
32 raw: String,
33}
34
35impl EnvValue {
36 pub(super) fn key(&self) -> EnvKey {
37 self.key
38 }
39
40 pub(super) fn as_str(&self) -> &str {
41 &self.raw
42 }
43
44 fn into_raw(self) -> String {
45 self.raw
46 }
47}
48
49pub(super) struct Env<'a> {
50 lookup: Box<Lookup<'a>>,
51}
52
53impl<'a> Env<'a> {
54 pub(super) fn new(get_var: impl Fn(&str) -> Option<String> + 'a) -> Self {
55 Self {
56 lookup: Box::new(get_var),
57 }
58 }
59
60 pub(super) fn var<T>(&self, key: &'static str) -> Var<'a, '_, T> {
61 Var {
62 lookup: self.lookup.as_ref(),
63 key: EnvKey::new(key),
64 checks: Vec::new(),
65 aliases: Vec::new(),
66 }
67 }
68}
69
70pub(super) struct Var<'env, 'lookup, T> {
71 lookup: &'lookup Lookup<'env>,
72 key: EnvKey,
73 checks: Vec<fn(EnvKey, T) -> Result<T>>,
74 aliases: Vec<EnvKey>,
75}
76
77impl<T> Var<'_, '_, T>
78where
79 T: EnvParse,
80{
81 pub(super) fn check(mut self, check: fn(EnvKey, T) -> Result<T>) -> Self {
82 self.checks.push(check);
83 self
84 }
85
86 pub(super) fn alias(mut self, alias: &'static str) -> Self {
87 self.aliases.push(EnvKey::new(alias));
88 self
89 }
90
91 pub(super) fn required(self) -> Result<T> {
92 let value = self
93 .load()
94 .with_context(|| format!("{} env variable is required", self.key))?;
95 self.parse(value)
96 }
97
98 pub(super) fn default(self, default: T) -> Result<T> {
99 let Some(value) = self.load() else {
100 return self.validate(self.key, default);
101 };
102 self.parse(value)
103 }
104
105 pub(super) fn optional(self) -> Result<Option<T>> {
106 self.load().map(|value| self.parse(value)).transpose()
107 }
108
109 fn load(&self) -> Option<EnvValue> {
110 self.load_key(self.key).or_else(|| {
111 self.aliases
112 .iter()
113 .copied()
114 .find_map(|alias| self.load_key(alias))
115 })
116 }
117
118 fn load_key(&self, key: EnvKey) -> Option<EnvValue> {
119 (self.lookup)(key.as_str()).map(|raw| EnvValue { key, raw })
120 }
121
122 fn parse(&self, value: EnvValue) -> Result<T> {
123 let key = value.key();
124 self.validate(key, T::parse(value)?)
125 }
126
127 fn validate(&self, key: EnvKey, mut value: T) -> Result<T> {
128 for check in &self.checks {
129 value = check(key, value)?;
130 }
131 Ok(value)
132 }
133}
134
135pub(super) trait EnvParse: Sized {
136 fn parse(value: EnvValue) -> Result<Self>;
137}
138
139macro_rules! parse_from_str {
140 ($type:ty, $name:literal) => {
141 impl EnvParse for $type {
142 fn parse(value: EnvValue) -> Result<Self> {
143 let key = value.key();
144 value
145 .into_raw()
146 .parse()
147 .map_err(|_error| anyhow!("{key} must be a valid {}", $name))
148 }
149 }
150 };
151}
152
153parse_from_str!(IpAddr, "IP address");
154parse_from_str!(SocketAddr, "socket address");
155parse_from_str!(u8, "u8");
156parse_from_str!(u16, "u16");
157parse_from_str!(u64, "u64");
158parse_from_str!(usize, "usize");
159
160impl EnvParse for bool {
161 fn parse(value: EnvValue) -> Result<Self> {
162 let key = value.key();
163 value
164 .into_raw()
165 .parse()
166 .map_err(|_error| anyhow!("{key} must be either `true` or `false`"))
167 }
168}
169
170impl EnvParse for String {
171 fn parse(value: EnvValue) -> Result<Self> {
172 Ok(value.into_raw())
173 }
174}
175
176impl EnvParse for Duration {
177 fn parse(value: EnvValue) -> Result<Self> {
178 let key = value.key();
179 let seconds = value
180 .into_raw()
181 .parse()
182 .map_err(|_error| anyhow!("{key} must be a valid duration in seconds"))?;
183 Ok(Self::from_secs(seconds))
184 }
185}
186
187pub(super) fn positive<T>(key: EnvKey, value: T) -> Result<T>
188where
189 T: From<u8> + PartialOrd,
190{
191 ensure!(value > T::from(0), "{key} must be greater than zero");
192 Ok(value)
193}
194
195pub(super) fn non_empty(key: EnvKey, value: String) -> Result<String> {
196 let trimmed = value.trim();
197 ensure!(!trimmed.is_empty(), "{key} must not be empty");
198 if trimmed.len() == value.len() {
199 Ok(value)
200 } else {
201 Ok(trimmed.to_owned())
202 }
203}
204
205#[cfg(test)]
206#[path = "TESTS/env.rs"]
207mod tests;