cat_gateway/settings/
str_env_var.rs

1//! Processing for String Environment Variables
2
3// cspell: words smhdwy
4
5use std::{
6    env::{self, VarError},
7    fmt::{self, Display},
8    str::FromStr,
9    time::Duration,
10};
11
12use duration_string::DurationString;
13use strum::VariantNames;
14use tracing::{error, info};
15
16/// An environment variable read as a string.
17#[derive(Clone)]
18pub(crate) struct StringEnvVar {
19    /// Value of the env var.
20    value: String,
21    /// Whether the env var is displayed redacted or not.
22    redacted: bool,
23}
24
25/// Ergonomic way of specifying if a env var needs to be redacted or not.
26pub(super) enum StringEnvVarParams {
27    /// The env var is plain and should not be redacted.
28    Plain(String, Option<String>),
29    /// The env var is redacted and should be redacted.
30    Redacted(String, Option<String>),
31}
32
33impl From<&str> for StringEnvVarParams {
34    fn from(s: &str) -> Self {
35        StringEnvVarParams::Plain(String::from(s), None)
36    }
37}
38
39impl From<String> for StringEnvVarParams {
40    fn from(s: String) -> Self {
41        StringEnvVarParams::Plain(s, None)
42    }
43}
44
45impl From<(&str, bool)> for StringEnvVarParams {
46    fn from((s, r): (&str, bool)) -> Self {
47        if r {
48            StringEnvVarParams::Redacted(String::from(s), None)
49        } else {
50            StringEnvVarParams::Plain(String::from(s), None)
51        }
52    }
53}
54
55impl From<(&str, bool, &str)> for StringEnvVarParams {
56    fn from((s, r, c): (&str, bool, &str)) -> Self {
57        if r {
58            StringEnvVarParams::Redacted(String::from(s), Some(String::from(c)))
59        } else {
60            StringEnvVarParams::Plain(String::from(s), Some(String::from(c)))
61        }
62    }
63}
64
65/// An environment variable read as a string.
66impl StringEnvVar {
67    /// Read the env var from the environment.
68    ///
69    /// If not defined, read from a .env file.
70    /// If still not defined, use the default.
71    ///
72    /// # Arguments
73    ///
74    /// * `var_name`: &str - the name of the env var
75    /// * `default_value`: &str - the default value
76    ///
77    /// # Returns
78    ///
79    /// * Self - the value
80    ///
81    /// # Example
82    ///
83    /// ```rust,no_run
84    /// #use cat_data_service::settings::StringEnvVar;
85    ///
86    /// let var = StringEnvVar::new("MY_VAR", "default");
87    /// assert_eq!(var.as_str(), "default");
88    /// ```
89    pub(super) fn new(
90        var_name: &str,
91        param: StringEnvVarParams,
92    ) -> Self {
93        let (default_value, redacted, choices) = match param {
94            StringEnvVarParams::Plain(s, c) => (s, false, c),
95            StringEnvVarParams::Redacted(s, c) => (s, true, c),
96        };
97
98        match env::var(var_name) {
99            Ok(value) => {
100                let value = Self { value, redacted };
101                info!(env=var_name, value=%value, "Env Var Defined");
102                value
103            },
104            Err(err) => {
105                let value = Self {
106                    value: default_value,
107                    redacted,
108                };
109                if err == VarError::NotPresent {
110                    if let Some(choices) = choices {
111                        info!(env=var_name, default=%value, choices=choices, "Env Var Defaulted");
112                    } else {
113                        info!(env=var_name, default=%value, "Env Var Defaulted");
114                    }
115                } else if let Some(choices) = choices {
116                    info!(env=var_name, default=%value, choices=choices, error=?err,
117                        "Env Var Error");
118                } else {
119                    info!(env=var_name, default=%value, error=?err, "Env Var Error");
120                }
121
122                value
123            },
124        }
125    }
126
127    /// New Env Var that is optional.
128    pub(super) fn new_optional(
129        var_name: &str,
130        redacted: bool,
131    ) -> Option<Self> {
132        match env::var(var_name) {
133            Ok(value) => {
134                let value = Self { value, redacted };
135                info!(env = var_name, value = %value, "Env Var Defined");
136                Some(value)
137            },
138            Err(VarError::NotPresent) => {
139                info!(env = var_name, "Env Var Not Set");
140                None
141            },
142            Err(error) => {
143                error!(env = var_name, error = ?error, "Env Var Error");
144                None
145            },
146        }
147    }
148
149    /// Convert an Envvar into the required Enum Type.
150    pub(super) fn new_as_enum<T: FromStr + Display + VariantNames>(
151        var_name: &str,
152        default: T,
153        redacted: bool,
154    ) -> T
155    where
156        <T as std::str::FromStr>::Err: std::fmt::Display,
157    {
158        let mut choices = String::new();
159        for name in T::VARIANTS {
160            if choices.is_empty() {
161                choices.push('[');
162            } else {
163                choices.push(',');
164            }
165            choices.push_str(name);
166        }
167        choices.push(']');
168
169        let choice = StringEnvVar::new(
170            var_name,
171            (default.to_string().as_str(), redacted, choices.as_str()).into(),
172        );
173
174        let value = match T::from_str(choice.as_str()) {
175            Ok(var) => var,
176            Err(error) => {
177                error!(error=%error, default=%default, choices=choices, choice=%choice,
178                    "Invalid choice. Using Default.");
179                default
180            },
181        };
182
183        value
184    }
185
186    /// Convert an Envvar into the required Duration type.
187    pub(crate) fn new_as_duration(
188        var_name: &str,
189        default: Duration,
190    ) -> Duration {
191        let choices = "A value in the format of `[0-9]+(ns|us|ms|[smhdwy])`";
192        let default = DurationString::new(default);
193
194        let raw_value = StringEnvVar::new(
195            var_name,
196            (default.to_string().as_str(), false, choices).into(),
197        )
198        .as_string();
199
200        DurationString::try_from(raw_value.clone())
201            .inspect_err(|err| {
202                error!(
203                    error = ?err,
204                    default = ?default,
205                    duration_str = raw_value,
206                    "Invalid Duration string. Defaulting to default value.",
207                );
208            })
209            .unwrap_or(default)
210            .into()
211    }
212
213    /// Convert an Envvar into the required Duration type.
214    pub(crate) fn new_as_duration_optional(var_name: &str) -> Option<Duration> {
215        let choices = "A value in the format of `[0-9]+(ns|us|ms|[smhdwy])`";
216
217        let raw_value = Self::new_optional(var_name, false)?.as_string();
218
219        DurationString::try_from(raw_value.clone())
220            .inspect_err(|err| {
221                error!(
222                    error = ?err,
223                    choices=choices,
224                    duration_str = raw_value,
225                    "Invalid Duration string. Defaulting to None",
226                );
227            })
228            .map(Into::into)
229            .ok()
230    }
231
232    /// Convert an Envvar into an integer in the bounded range.
233    pub(super) fn new_as_int<T>(
234        var_name: &str,
235        default: T,
236        min: T,
237        max: T,
238    ) -> T
239    where
240        T: FromStr + Display + PartialOrd + tracing::Value,
241        <T as std::str::FromStr>::Err: std::fmt::Display,
242    {
243        let choices = format!("A value in the range {min} to {max} inclusive");
244
245        let raw_value = StringEnvVar::new(
246            var_name,
247            (default.to_string().as_str(), false, choices.as_str()).into(),
248        )
249        .as_string();
250
251        match raw_value.parse::<T>() {
252            Ok(value) => {
253                if value < min {
254                    error!("{var_name} out of range. Range = {min} to {max} inclusive. Clamped to {min}");
255                    min
256                } else if value > max {
257                    error!("{var_name} out of range. Range = {min} to {max} inclusive. Clamped to {max}");
258                    max
259                } else {
260                    value
261                }
262            },
263            Err(error) => {
264                error!(error=%error, default=default, "{var_name} not an integer. Range = {min} to {max} inclusive. Defaulted");
265                default
266            },
267        }
268    }
269
270    /// Get the read env var as a str.
271    ///
272    /// # Returns
273    ///
274    /// * &str - the value
275    pub(crate) fn as_str(&self) -> &str {
276        &self.value
277    }
278
279    /// Get the read env var as a str.
280    ///
281    /// # Returns
282    ///
283    /// * &str - the value
284    pub(crate) fn as_string(&self) -> String {
285        self.value.clone()
286    }
287}
288
289impl fmt::Display for StringEnvVar {
290    fn fmt(
291        &self,
292        f: &mut fmt::Formatter<'_>,
293    ) -> fmt::Result {
294        if self.redacted {
295            return write!(f, "REDACTED");
296        }
297        write!(f, "{}", self.value)
298    }
299}
300
301impl fmt::Debug for StringEnvVar {
302    fn fmt(
303        &self,
304        f: &mut fmt::Formatter<'_>,
305    ) -> fmt::Result {
306        if self.redacted {
307            return write!(f, "REDACTED");
308        }
309        write!(f, "env: {}", self.value)
310    }
311}