Skip to main content

o_sfu/config/
env.rs

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;