plugx_config/loader/
env.rs

1//! Environment-Variables configuration loader (`env` feature which is enabled by default).
2//!
3//! This is only usable if you enabled `env` Cargo feature.
4//!
5//! ### Example
6//! ```rust
7//! use std::env::set_var;
8//! use plugx_config::{
9//!     loader::{Loader, env::Env},
10//!     ext::url::Url,
11//! };
12//!
13//! set_var("MY_APP_NAME__FOO__B_A_R", "Baz");
14//! set_var("MY_APP_NAME__QUX__ABC", "XYZ");
15//!
16//! let url = Url::try_from("env://?prefix=MY_APP_NAME").expect("A valid URL!");
17//!
18//! let mut loader = Env::new();
19//! // You could set `prefix`, `separator`, and `strip_prefix` programmatically like this:
20//! // loader.[set|with]_prefix("MY_APP_NAME");
21//! // loader.[set|with]_separator("__");
22//! // loader.[set|with]_strip_prefix(true);
23//!
24//! // We do not set `whitelist` so we're going to load all plugins' configurations:
25//! let mut maybe_whitelist = None;
26//! let result = loader.load(&url, maybe_whitelist, false).unwrap();
27//! let (_, foo) = result
28//!     .iter()
29//!     .find(|(plugin_name, _)| plugin_name == "foo")
30//!     .expect("`foo` plugin config");
31//! assert_eq!(foo.maybe_contents(), Some(&"B_A_R=\"Baz\"".to_string()));
32//! let (_, qux) = result
33//!     .iter()
34//!     .find(|(plugin_name, _)| plugin_name == "qux")
35//!     .expect("`qux` plugin config");
36//! assert_eq!(qux.maybe_contents(), Some(&"ABC=\"XYZ\"".to_string()));
37//!
38//! // Only load `foo` plugin configuration:
39//! let whitelist = ["foo".to_string()].to_vec();
40//! maybe_whitelist = Some(&whitelist);
41//! let result = loader.load(&url, maybe_whitelist, false).unwrap();
42//! assert!(result.iter().find(|(plugin_name, _)| plugin_name == "foo").is_some());
43//! assert!(result.iter().find(|(plugin_name, _)| plugin_name == "qux").is_none());
44//! ```
45//!
46//! See [mod@loader] documentation to known how loaders work.
47
48use crate::{
49    entity::ConfigurationEntity,
50    loader::{self, Error, Loader},
51};
52use cfg_if::cfg_if;
53use serde::Deserialize;
54use std::fmt::{Display, Formatter};
55use std::{env, fmt::Debug};
56use url::Url;
57
58pub const NAME: &str = "Environment-Variables";
59pub const SCHEME_LIST: &[&str] = &["env"];
60
61/// Loads configurations from Environment-Variables.
62#[derive(Debug, Default, Clone)]
63pub struct Env {
64    options: EnvOptions,
65}
66
67#[derive(Debug, Clone, Deserialize)]
68#[serde(default)]
69struct EnvOptions {
70    prefix: String,
71    separator: String,
72    strip_prefix: bool,
73}
74
75impl Default for EnvOptions {
76    fn default() -> Self {
77        Self {
78            prefix: default::prefix(),
79            separator: default::separator(),
80            strip_prefix: default::strip_prefix(),
81        }
82    }
83}
84
85pub mod default {
86
87    #[inline]
88    pub fn prefix() -> String {
89        let mut prefix = option_env!("CARGO_BIN_NAME").unwrap_or("").to_string();
90        if prefix.is_empty() {
91            prefix = option_env!("CARGO_CRATE_NAME").unwrap_or("").to_string();
92        }
93        if !prefix.is_empty() {
94            prefix += separator().as_str();
95        }
96        prefix
97    }
98
99    #[inline(always)]
100    pub fn separator() -> String {
101        "__".to_string()
102    }
103
104    #[inline(always)]
105    pub fn strip_prefix() -> bool {
106        true
107    }
108}
109
110impl Env {
111    /// Same as `default()` method.
112    pub fn new() -> Self {
113        Default::default()
114    }
115
116    /// Only loads keys with this prefix.
117    pub fn set_prefix<P: AsRef<str>>(&mut self, prefix: P) {
118        self.options.prefix = prefix.as_ref().to_string();
119    }
120
121    /// Only loads keys with this prefix.
122    pub fn with_prefix<P: AsRef<str>>(mut self, prefix: P) -> Self {
123        self.set_prefix(prefix);
124        self
125    }
126
127    /// Used is separating plugin names.
128    pub fn set_separator<S: AsRef<str>>(&mut self, separator: S) {
129        self.options.separator = separator.as_ref().to_string();
130    }
131
132    /// Used is separating plugin names.
133    pub fn with_separator<S: AsRef<str>>(mut self, separator: S) -> Self {
134        self.set_separator(separator);
135        self
136    }
137
138    /// Used is separating plugin names.
139    pub fn set_strip_prefix(&mut self, strip_prefix: bool) {
140        self.options.strip_prefix = strip_prefix;
141    }
142
143    /// Used is separating plugin names.
144    pub fn with_strip_prefix(mut self, strip_prefix: bool) -> Self {
145        self.set_strip_prefix(strip_prefix);
146        self
147    }
148}
149
150impl Display for Env {
151    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
152        f.write_str(NAME)
153    }
154}
155
156impl Loader for Env {
157    /// In this case `["env"]`.
158    fn scheme_list(&self) -> Vec<String> {
159        SCHEME_LIST.iter().cloned().map(String::from).collect()
160    }
161
162    /// This loader does not support `skip_soft_errors`.  
163    fn load(
164        &self,
165        url: &Url,
166        maybe_whitelist: Option<&[String]>,
167        _skip_soft_errors: bool,
168    ) -> Result<Vec<(String, ConfigurationEntity)>, Error> {
169        let EnvOptions {
170            mut prefix,
171            mut separator,
172            mut strip_prefix,
173        } = loader::deserialize_query_string(NAME, url)?;
174        if self.options.prefix != default::prefix() {
175            prefix = self.options.prefix.clone()
176        }
177        if self.options.separator != default::separator() {
178            separator = self.options.separator.clone()
179        }
180        if self.options.strip_prefix != default::strip_prefix() {
181            strip_prefix = self.options.strip_prefix
182        }
183        if !separator.is_empty() && !prefix.is_empty() && !prefix.ends_with(separator.as_str()) {
184            prefix += separator.as_str()
185        }
186        let mut result = Vec::new();
187        env::vars()
188            .filter(|(key, _)| prefix.is_empty() || key.starts_with(prefix.as_str()))
189            .map(|(mut key, value)| {
190                if !prefix.is_empty() && strip_prefix {
191                    key = key.chars().skip(prefix.chars().count()).collect::<String>()
192                }
193                (key, value)
194            })
195            .filter(|(key, _)| !key.is_empty())
196            .map(|(key, value)| {
197                let key_list = if separator.is_empty() {
198                    [key].to_vec()
199                } else {
200                    key.splitn(2, separator.as_str())
201                        .map(|key| key.to_string())
202                        .collect()
203                };
204                (key_list, value)
205            })
206            .filter(|(key_list, _)| !key_list[0].is_empty())
207            .map(|(mut key_list, value)| {
208                let plugin_name = key_list.remove(0).to_lowercase();
209                let key = if key_list.len() == 1 {
210                    key_list.remove(0)
211                } else {
212                    String::new()
213                };
214                (plugin_name, key, value)
215            })
216            .filter(|(_, key, _)| !key.is_empty())
217            .map(|(_plugin_name, _key, _value)| {
218                cfg_if! {
219                    if #[cfg(feature = "tracing")] {
220                        tracing::trace!(
221                            plugin=_plugin_name,
222                            key=_key,
223                            value=_value,
224                            "Detected environment-variable"
225                        );
226                    } else if #[cfg(feature = "logging")] {
227                        log::trace!(
228                            "msg=\"Detected environment-variable\" plugin={_plugin_name:?} key={_key:?} value={_value:?}"
229                        );
230                    }
231                }
232                (_plugin_name, _key, _value)
233            })
234            .filter(|(plugin_name, _, _)| {
235                maybe_whitelist
236                    .as_ref()
237                    .map(|whitelist| whitelist.contains(plugin_name))
238                    .unwrap_or(true)
239            })
240            .for_each(|(plugin_name, key, value)| {
241                let key_value = format!("{key}={value:?}");
242                if let Some((_, _, configuration)) =
243                    result.iter_mut().find(|(name, _, _)| *name == plugin_name)
244                {
245                    *configuration += "\n";
246                    *configuration += key_value.as_str();
247                } else {
248                    result.push((plugin_name, format!("{prefix}*"), key_value));
249                }
250            });
251        Ok(result
252            .into_iter()
253            .map(|(plugin_name, key, contents)| {
254                (
255                    plugin_name.clone(),
256                    ConfigurationEntity::new(key, url.clone(), plugin_name, NAME)
257                        .with_format("env")
258                        .with_contents(contents),
259                )
260            })
261            .map(|(_plugin_name, _configuration)| {
262                cfg_if! {
263                    if #[cfg(feature = "tracing")] {
264                        tracing::trace!(
265                            plugin=_plugin_name,
266                            format=_configuration.maybe_format().unwrap_or(&"<unknown>".to_string()),
267                            contents=_configuration.maybe_contents().unwrap(),
268                            "Detected configuration from environment-variable"
269                        );
270                    } else if #[cfg(feature = "logging")] {
271                        log::trace!(
272                            "msg=\"Detected configuration from environment-variable\" plugin={_plugin_name:?} format={:?} contents={:?}",
273                            _configuration.maybe_format().unwrap_or(&"<unknown>".to_string()),
274                            _configuration.maybe_contents().unwrap(),
275                        );
276                    }
277                }
278                (_plugin_name, _configuration)
279            })
280            .collect())
281    }
282}