storm_config/
env.rs

1use std::env;
2
3#[cfg(feature = "convert-case")]
4use convert_case::{Case, Casing};
5
6use crate::errors::Result;
7use crate::map::Map;
8use crate::source::Source;
9use crate::value::{Value, ValueKind};
10
11/// An environment source collects a dictionary of environment variables values into a hierarchical
12/// config Value type. We have to be aware how the config tree is created from the environment
13/// dictionary, therefore we are mindful about prefixes for the environment keys, level separators,
14/// encoding form (kebab, snake case) etc.
15#[must_use]
16#[derive(Clone, Debug, Default)]
17pub struct Environment {
18  /// Optional prefix that will limit access to the environment to only keys that
19  /// begin with the defined prefix.
20  ///
21  /// A prefix with a separator of `_` is tested to be present on each key before its considered
22  /// to be part of the source environment.
23  ///
24  /// For example, the key `CONFIG_DEBUG` would become `DEBUG` with a prefix of `config`.
25  prefix: Option<String>,
26
27  /// Optional character sequence that separates the prefix from the rest of the key
28  prefix_separator: Option<String>,
29
30  /// Optional character sequence that separates each key segment in an environment key pattern.
31  /// Consider a nested configuration such as `redis.password`, a separator of `_` would allow
32  /// an environment key of `REDIS_PASSWORD` to match.
33  separator: Option<String>,
34
35  /// Optional directive to translate collected keys into a form that matches what serializers
36  /// that the configuration would expect. For example if you have the `kebab-case` attribute
37  /// for your serde config types, you may want to pass Case::Kebab here.
38  #[cfg(feature = "convert-case")]
39  convert_case: Option<convert_case::Case>,
40
41  /// Optional character sequence that separates each env value into a vector. only works when try_parsing is set to true
42  /// Once set, you cannot have type String on the same environment, unless you set list_parse_keys.
43  list_separator: Option<String>,
44  /// A list of keys which should always be parsed as a list. If not set you can have only `Vec<String>` or `String` (not both) in one environment.
45  list_parse_keys: Option<Vec<String>>,
46
47  /// Ignore empty env values (treat as unset).
48  ignore_empty: bool,
49
50  /// Parses booleans, integers and floats if they're detected (can be safely parsed).
51  try_parsing: bool,
52
53  // Preserve the prefix while parsing
54  keep_prefix: bool,
55
56  /// Alternate source for the environment. This can be used when you want to test your own code
57  /// using this source, without the need to change the actual system environment variables.
58  ///
59  /// ## Example
60  ///
61  /// ```rust
62  /// use storm_config::{Environment, Config};
63  /// use serde::Deserialize;
64  /// use std::collections::HashMap;
65  /// use std::convert::TryInto;
66  /// #
67  /// #[test]
68  /// fn test_config() -> Result<(), config::ConfigError> {
69  ///   #[derive(Clone, Debug, Deserialize)]
70  ///   struct MyConfig {
71  ///     pub my_string: String,
72  ///   }
73  ///
74  ///   let source = Environment::default()
75  ///     .source(Some({
76  ///       let mut env = HashMap::new();
77  ///       env.insert("MY_STRING".into(), "my-value".into());
78  ///       env
79  ///   }));
80  ///
81  ///   let config: MyConfig = Config::builder()
82  ///     .add_source(source)
83  ///     .build()?
84  ///     .try_into()?;
85  ///   assert_eq!(config.my_string, "my-value");
86  ///
87  ///   Ok(())
88  /// }
89  /// ```
90  source: Option<Map<String, String>>,
91}
92
93impl Environment {
94  #[deprecated(since = "0.12.0", note = "please use 'Environment::default' instead")]
95  pub fn new() -> Self {
96    Self::default()
97  }
98
99  /// Optional prefix that will limit access to the environment to only keys that
100  /// begin with the defined prefix.
101  ///
102  /// A prefix with a separator of `_` is tested to be present on each key before its considered
103  /// to be part of the source environment.
104  ///
105  /// For example, the key `CONFIG_DEBUG` would become `DEBUG` with a prefix of `config`.
106  pub fn with_prefix(s: &str) -> Self {
107    Self { prefix: Some(s.into()), ..Self::default() }
108  }
109
110  /// See [Environment::with_prefix]
111  pub fn prefix(mut self, s: &str) -> Self {
112    self.prefix = Some(s.into());
113    self
114  }
115
116  #[cfg(feature = "convert-case")]
117  pub fn with_convert_case(tt: Case) -> Self {
118    Self::default().convert_case(tt)
119  }
120
121  #[cfg(feature = "convert-case")]
122  pub fn convert_case(mut self, tt: Case) -> Self {
123    self.convert_case = Some(tt);
124    self
125  }
126
127  /// Optional character sequence that separates the prefix from the rest of the key
128  pub fn prefix_separator(mut self, s: &str) -> Self {
129    self.prefix_separator = Some(s.into());
130    self
131  }
132
133  /// Optional character sequence that separates each key segment in an environment key pattern.
134  /// Consider a nested configuration such as `redis.password`, a separator of `_` would allow
135  /// an environment key of `REDIS_PASSWORD` to match.
136  pub fn separator(mut self, s: &str) -> Self {
137    self.separator = Some(s.into());
138    self
139  }
140
141  /// When set and try_parsing is true, then all environment variables will be parsed as [`Vec<String>`] instead of [`String`].
142  /// See
143  /// [`with_list_parse_key`](Self::with_list_parse_key)
144  /// when you want to use [`Vec<String>`] in combination with [`String`].
145  pub fn list_separator(mut self, s: &str) -> Self {
146    self.list_separator = Some(s.into());
147    self
148  }
149
150  /// Add a key which should be parsed as a list when collecting [`Value`]s from the environment.
151  /// Once list_separator is set, the type for string is [`Vec<String>`].
152  /// To switch the default type back to type Strings you need to provide the keys which should be [`Vec<String>`] using this function.
153  pub fn with_list_parse_key(mut self, key: &str) -> Self {
154    if self.list_parse_keys.is_none() {
155      self.list_parse_keys = Some(vec![key.to_lowercase()])
156    } else {
157      self.list_parse_keys = self.list_parse_keys.map(|mut keys| {
158        keys.push(key.to_lowercase());
159        keys
160      });
161    }
162    self
163  }
164
165  /// Ignore empty env values (treat as unset).
166  pub fn ignore_empty(mut self, ignore: bool) -> Self {
167    self.ignore_empty = ignore;
168    self
169  }
170
171  /// Note: enabling `try_parsing` can reduce performance it will try and parse
172  /// each environment variable 3 times (bool, i64, f64)
173  pub fn try_parsing(mut self, try_parsing: bool) -> Self {
174    self.try_parsing = try_parsing;
175    self
176  }
177
178  // Preserve the prefix while parsing
179  pub fn keep_prefix(mut self, keep: bool) -> Self {
180    self.keep_prefix = keep;
181    self
182  }
183
184  /// Alternate source for the environment. This can be used when you want to test your own code
185  /// using this source, without the need to change the actual system environment variables.
186  ///
187  /// ## Example
188  ///
189  /// ```rust
190  /// use storm_config::{Environment, Config};
191  /// use serde::Deserialize;
192  /// use std::collections::HashMap;
193  /// use std::convert::TryInto;
194  /// #
195  /// #[test]
196  /// fn test_config() -> Result<(), config::ConfigError> {
197  ///   #[derive(Clone, Debug, Deserialize)]
198  ///   struct MyConfig {
199  ///     pub my_string: String,
200  ///   }
201  ///
202  ///   let source = Environment::default()
203  ///     .source(Some({
204  ///       let mut env = HashMap::new();
205  ///       env.insert("MY_STRING".into(), "my-value".into());
206  ///       env
207  ///   }));
208  ///
209  ///   let config: MyConfig = Config::builder()
210  ///     .add_source(source)
211  ///     .build()?
212  ///     .try_into()?;
213  ///   assert_eq!(config.my_string, "my-value");
214  ///
215  ///   Ok(())
216  /// }
217  /// ```
218  pub fn source(mut self, source: Option<Map<String, String>>) -> Self {
219    self.source = source;
220    self
221  }
222}
223
224impl Source for Environment {
225  fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
226    Box::new((*self).clone())
227  }
228
229  fn collect(&self) -> Result<Map<String, Value>> {
230    let mut m = Map::new();
231    let uri: String = "the environment".into();
232
233    let separator = self.separator.as_deref().unwrap_or("");
234    #[cfg(feature = "convert-case")]
235    let convert_case = &self.convert_case;
236    let prefix_separator = match (self.prefix_separator.as_deref(), self.separator.as_deref()) {
237      (Some(pre), _) => pre,
238      (None, Some(sep)) => sep,
239      (None, None) => "_",
240    };
241
242    // Define a prefix pattern to test and exclude from keys
243    let prefix_pattern =
244      self.prefix.as_ref().map(|prefix| format!("{}{}", prefix, prefix_separator).to_lowercase());
245
246    let collector = |(key, value): (String, String)| {
247      // Treat empty environment variables as unset
248      if self.ignore_empty && value.is_empty() {
249        return;
250      }
251
252      let mut key = key.to_lowercase();
253
254      // Check for prefix
255      if let Some(ref prefix_pattern) = prefix_pattern {
256        if key.starts_with(prefix_pattern) {
257          if !self.keep_prefix {
258            // Remove this prefix from the key
259            key = key[prefix_pattern.len()..].to_string();
260          }
261        } else {
262          // Skip this key
263          return;
264        }
265      }
266
267      // If separator is given replace with `.`
268      if !separator.is_empty() {
269        key = key.replace(separator, ".");
270      }
271
272      #[cfg(feature = "convert-case")]
273      if let Some(convert_case) = convert_case {
274        key = key.to_case(*convert_case);
275      }
276
277      let value = if self.try_parsing {
278        // convert to lowercase because bool parsing expects all lowercase
279        if let Ok(parsed) = value.to_lowercase().parse::<bool>() {
280          ValueKind::Boolean(parsed)
281        } else if let Ok(parsed) = value.parse::<i64>() {
282          ValueKind::I64(parsed)
283        } else if let Ok(parsed) = value.parse::<f64>() {
284          ValueKind::Float(parsed)
285        } else if let Some(separator) = &self.list_separator {
286          if let Some(keys) = &self.list_parse_keys {
287            #[cfg(feature = "convert-case")]
288            let key = key.to_lowercase();
289
290            if keys.contains(&key) {
291              let v: Vec<Value> = value
292                .split(separator)
293                .map(|s| Value::new(Some(&uri), ValueKind::String(s.to_string())))
294                .collect();
295              ValueKind::Array(v)
296            } else {
297              ValueKind::String(value)
298            }
299          } else {
300            let v: Vec<Value> = value
301              .split(separator)
302              .map(|s| Value::new(Some(&uri), ValueKind::String(s.to_string())))
303              .collect();
304            ValueKind::Array(v)
305          }
306        } else {
307          ValueKind::String(value)
308        }
309      } else {
310        ValueKind::String(value)
311      };
312
313      m.insert(key, Value::new(Some(&uri), value));
314    };
315
316    match &self.source {
317      Some(source) => source.clone().into_iter().for_each(collector),
318      None => env::vars().for_each(collector),
319    }
320
321    Ok(m)
322  }
323}