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(var_name: &str, param: StringEnvVarParams) -> Self {
90        let (default_value, redacted, choices) = match param {
91            StringEnvVarParams::Plain(s, c) => (s, false, c),
92            StringEnvVarParams::Redacted(s, c) => (s, true, c),
93        };
94
95        match env::var(var_name) {
96            Ok(value) => {
97                let value = Self { value, redacted };
98                info!(env=var_name, value=%value, "Env Var Defined");
99                value
100            },
101            Err(err) => {
102                let value = Self {
103                    value: default_value,
104                    redacted,
105                };
106                if err == VarError::NotPresent {
107                    if let Some(choices) = choices {
108                        info!(env=var_name, default=%value, choices=choices, "Env Var Defaulted");
109                    } else {
110                        info!(env=var_name, default=%value, "Env Var Defaulted");
111                    }
112                } else if let Some(choices) = choices {
113                    info!(env=var_name, default=%value, choices=choices, error=?err,
114                        "Env Var Error");
115                } else {
116                    info!(env=var_name, default=%value, error=?err, "Env Var Error");
117                }
118
119                value
120            },
121        }
122    }
123
124    /// New Env Var that is optional.
125    pub(super) fn new_optional(var_name: &str, redacted: bool) -> Option<Self> {
126        match env::var(var_name) {
127            Ok(value) => {
128                let value = Self { value, redacted };
129                info!(env = var_name, value = %value, "Env Var Defined");
130                Some(value)
131            },
132            Err(VarError::NotPresent) => {
133                info!(env = var_name, "Env Var Not Set");
134                None
135            },
136            Err(error) => {
137                error!(env = var_name, error = ?error, "Env Var Error");
138                None
139            },
140        }
141    }
142
143    /// Convert an Envvar into the required Enum Type.
144    pub(super) fn new_as_enum<T: FromStr + Display + VariantNames>(
145        var_name: &str, default: T, redacted: bool,
146    ) -> T
147    where <T as std::str::FromStr>::Err: std::fmt::Display {
148        let mut choices = String::new();
149        for name in T::VARIANTS {
150            if choices.is_empty() {
151                choices.push('[');
152            } else {
153                choices.push(',');
154            }
155            choices.push_str(name);
156        }
157        choices.push(']');
158
159        let choice = StringEnvVar::new(
160            var_name,
161            (default.to_string().as_str(), redacted, choices.as_str()).into(),
162        );
163
164        let value = match T::from_str(choice.as_str()) {
165            Ok(var) => var,
166            Err(error) => {
167                error!(error=%error, default=%default, choices=choices, choice=%choice,
168                    "Invalid choice. Using Default.");
169                default
170            },
171        };
172
173        value
174    }
175
176    /// Convert an Envvar into the required Duration type.
177    pub(crate) fn new_as_duration(var_name: &str, default: Duration) -> Duration {
178        let choices = "A value in the format of `[0-9]+(ns|us|ms|[smhdwy])`";
179        let default = DurationString::new(default);
180
181        let raw_value = StringEnvVar::new(
182            var_name,
183            (default.to_string().as_str(), false, choices).into(),
184        )
185        .as_string();
186
187        DurationString::try_from(raw_value.clone())
188            .inspect(|err| {
189                error!("Invalid Duration: {raw_value} : {err}. Defaulting to {default}.",);
190            })
191            .unwrap_or(default)
192            .into()
193    }
194
195    /// Convert an Envvar into an integer in the bounded range.
196    pub(super) fn new_as_int<T>(var_name: &str, default: T, min: T, max: T) -> T
197    where
198        T: FromStr + Display + PartialOrd + tracing::Value,
199        <T as std::str::FromStr>::Err: std::fmt::Display,
200    {
201        let choices = format!("A value in the range {min} to {max} inclusive");
202
203        let raw_value = StringEnvVar::new(
204            var_name,
205            (default.to_string().as_str(), false, choices.as_str()).into(),
206        )
207        .as_string();
208
209        match raw_value.parse::<T>() {
210            Ok(value) => {
211                if value < min {
212                    error!("{var_name} out of range. Range = {min} to {max} inclusive. Clamped to {min}");
213                    min
214                } else if value > max {
215                    error!("{var_name} out of range. Range = {min} to {max} inclusive. Clamped to {max}");
216                    max
217                } else {
218                    value
219                }
220            },
221            Err(error) => {
222                error!(error=%error, default=default, "{var_name} not an integer. Range = {min} to {max} inclusive. Defaulted");
223                default
224            },
225        }
226    }
227
228    /// Get the read env var as a str.
229    ///
230    /// # Returns
231    ///
232    /// * &str - the value
233    pub(crate) fn as_str(&self) -> &str {
234        &self.value
235    }
236
237    /// Get the read env var as a str.
238    ///
239    /// # Returns
240    ///
241    /// * &str - the value
242    pub(crate) fn as_string(&self) -> String {
243        self.value.clone()
244    }
245}
246
247impl fmt::Display for StringEnvVar {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        if self.redacted {
250            return write!(f, "REDACTED");
251        }
252        write!(f, "{}", self.value)
253    }
254}
255
256impl fmt::Debug for StringEnvVar {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        if self.redacted {
259            return write!(f, "REDACTED");
260        }
261        write!(f, "env: {}", self.value)
262    }
263}