plugx_config/parser/
env.rs

1//! Environment-Variable configuration parser.
2//!
3//! This is only usable if you enabled `env` Cargo feature.
4//!
5//! ### Example
6//! ```rust
7//! use plugx_config::parser::{Parser, env::Env};
8//! use plugx_input::Input;
9//!
10//! let bytes = br#"
11//! FOO__BAR__BAZ=Qux
12//! FOO__BAR__ABC=3.14 # Comments are supported!
13//! FOO__XYZ=false
14//! HELLO='["w", "o", "l", "d"]' # A JSON list
15//! "#;
16//!
17//! let parser = Env::new();
18//! // You can set nested key separator like this:
19//! // parser.set_key_separator("__");
20//! let parsed: Input = parser.parse(bytes.as_slice()).unwrap();
21//! assert!(
22//!     parsed.as_map().len() == 2 &&
23//!     parsed.as_map().contains_key("foo") &&
24//!     parsed.as_map().contains_key("hello")
25//! );
26//! let foo = parsed.as_map().get("foo").unwrap();
27//! assert!(
28//!     foo.as_map().len() == 2 &&
29//!     foo.as_map().contains_key("bar") &&
30//!     foo.as_map().contains_key("xyz")
31//! );
32//! let bar = foo.as_map().get("bar").unwrap();
33//! assert_eq!(bar.as_map().get("baz").unwrap(), &"Qux".into());
34//! assert_eq!(bar.as_map().get("abc").unwrap(), &3.14.into());
35//! let xyz = foo.as_map().get("xyz").unwrap();
36//! assert_eq!(xyz, &false.into());
37//! let list = ["w", "o", "l", "d"].into();
38//! assert_eq!(parsed.as_map().get("hello").unwrap(), &list);
39//! ```
40//!
41
42use crate::parser::Parser;
43use anyhow::{anyhow, bail};
44use cfg_if::cfg_if;
45use plugx_input::{position, position::InputPosition, Input};
46use std::fmt::{Debug, Display, Formatter};
47
48#[derive(Debug, Clone)]
49pub struct Env {
50    separator: String,
51}
52
53impl Default for Env {
54    fn default() -> Self {
55        Self {
56            separator: crate::loader::env::default::separator(),
57        }
58    }
59}
60
61impl Display for Env {
62    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
63        f.write_str("Environment-Variables")
64    }
65}
66
67impl Parser for Env {
68    fn supported_format_list(&self) -> Vec<String> {
69        ["env".into()].into()
70    }
71
72    fn try_parse(&self, bytes: &[u8]) -> anyhow::Result<Input> {
73        let text = String::from_utf8(bytes.to_vec())
74            .map_err(|error| anyhow!("Could not decode contents to UTF-8 ({error})"))?;
75        let mut list = dotenv_parser::parse_dotenv(text.as_str())
76            .map_err(|error| anyhow!(error))?
77            .into_iter()
78            .collect::<Vec<(String, String)>>();
79        list.sort_by_key(|(key, _)| key.to_string());
80
81        let mut map = Input::new_map();
82        update_input_from_env(
83            &mut map,
84            list.into_iter()
85                .map(|(key, value)| {
86                    (
87                        key.split(self.separator.as_str())
88                            .map(|key| key.to_lowercase())
89                            .collect::<Vec<String>>(),
90                        value,
91                    )
92                })
93                .collect::<Vec<_>>()
94                .as_slice(),
95        )
96        .map_err(|error| anyhow!(error))?;
97        cfg_if! {
98            if #[cfg(feature = "tracing")] {
99                tracing::trace!(
100                    input=text,
101                    output=%map,
102                    "Parsed environment-variable contents"
103                );
104            } else if #[cfg(feature = "logging")] {
105                log::trace!("msg=\"Parsed environment-variable contents\" input={text:?} output={:?}", map.to_string());
106            }
107        }
108        Ok(map)
109    }
110
111    fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
112        if let Ok(text) = String::from_utf8(bytes.to_vec()) {
113            Some(dotenv_parser::parse_dotenv(text.as_str()).is_ok())
114        } else {
115            Some(false)
116        }
117    }
118}
119
120impl Env {
121    pub fn new() -> Self {
122        Default::default()
123    }
124
125    pub fn set_key_separator<K: AsRef<str>>(&mut self, key_separator: K) {
126        self.separator = key_separator.as_ref().to_string();
127    }
128
129    pub fn with_key_separator<K: AsRef<str>>(mut self, key_separator: K) -> Self {
130        self.set_key_separator(key_separator);
131        self
132    }
133}
134
135fn update_input_from_env(
136    input: &mut Input,
137    env_list: &[(Vec<String>, String)],
138) -> anyhow::Result<()> {
139    for (key_list, value) in env_list {
140        if key_list.is_empty() || key_list.iter().any(|key| key.is_empty()) {
141            continue;
142        }
143        update_input_from_key_list(input, key_list, value.clone(), position::new())?;
144    }
145    Ok(())
146}
147
148fn update_input_from_key_list(
149    input: &mut Input,
150    key_list: &[String],
151    value: String,
152    position: InputPosition,
153) -> anyhow::Result<()> {
154    if key_list.len() == 1 {
155        let value = if let Ok(value) = serde_json::from_str::<Input>(value.as_str()) {
156            value
157        } else {
158            Input::from(value.clone())
159        };
160        let key = key_list[0].clone();
161        input.map_mut().insert(key, value);
162        Ok(())
163    } else {
164        let (key, key_list) = key_list.split_first().unwrap();
165        let position = position.new_with_key(key);
166        if !input.as_map().contains_key(key) {
167            input.map_mut().insert(key.clone(), Input::new_map());
168        }
169        let inner_input = input.map_mut().get_mut(key).unwrap();
170        if inner_input.is_map() {
171            update_input_from_key_list(inner_input, key_list, value, position)
172        } else {
173            bail!(
174                "{position} already exists with type {}, but we needed {}",
175                inner_input.type_name(),
176                Input::map_type_name()
177            )
178        }
179    }
180}