use std::{borrow::Cow, ops::Not};
use serde::de::DeserializeOwned;
use crate::{error, error::EnvDeserializationError, value::Parser, Value};
#[derive(Debug, Clone)]
#[must_use]
pub struct Config<'a> {
prefix: Option<Cow<'a, str>>,
case_sensitive: bool,
separator: Cow<'a, str>,
}
impl Default for Config<'static> {
fn default() -> Self {
Self::new()
}
}
impl<'a> Config<'a> {
pub const fn new() -> Self {
Self {
prefix: None,
case_sensitive: false,
separator: Cow::Borrowed("__"),
}
}
pub fn with_separator<S>(&mut self, separator: S) -> &mut Self
where
S: Into<Cow<'a, str>>,
{
self.separator = separator.into();
self
}
pub fn with_prefix<S>(&mut self, prefix: S) -> &mut Self
where
S: Into<Cow<'a, str>>,
{
self.prefix = Some(prefix.into());
self
}
pub fn without_prefix(&mut self) -> &mut Self {
self.prefix = None;
self
}
pub fn case_sensitive(&mut self, case_sensitive: bool) -> &mut Self {
self.case_sensitive = case_sensitive;
self
}
pub fn build_from_env<T: DeserializeOwned>(&self) -> Result<T, error::EnvDeserializationError> {
let env_values = std::env::vars();
self.build_from_iter(env_values)
}
pub fn build_from_iter<T, K, V, I>(&self, iter: I) -> Result<T, error::EnvDeserializationError>
where
T: DeserializeOwned,
K: Into<String>,
V: Into<String>,
I: IntoIterator<Item = (K, V)>,
{
let values = iter.into_iter().map(|(k, v)| (k.into(), v.into()));
let values = values.filter_map(|(mut key, value)| {
if self.case_sensitive.not() {
key.make_ascii_lowercase();
}
let value = Value::Simple(value);
if let Some(prefix) = &self.prefix {
let coerced_prefix;
let prefix = if self.case_sensitive {
prefix.as_ref()
} else {
coerced_prefix = prefix.to_ascii_lowercase();
&coerced_prefix
};
let stripped_key = key.strip_prefix(prefix)?.to_owned();
Some((stripped_key, value))
} else {
Some((key, value))
}
});
let parser = self.create_parser(values)?;
T::deserialize(parser)
}
fn create_parser<I>(&self, iter: I) -> Result<Parser, EnvDeserializationError>
where
I: IntoIterator<Item = (String, Value)>,
{
let mut base = Value::Map(vec![]);
for (key, value) in iter.into_iter() {
let path = key.split(self.separator.as_ref()).collect::<Vec<_>>();
if path.len() == 1 {
if let Value::Map(base) = &mut base {
base.push((key, value));
} else {
unreachable!()
}
} else {
base.insert_at(&path, value)?;
}
}
Ok(Parser {
config: self,
current: base,
})
}
pub(crate) fn maybe_coerce_case<I, V>(
&self,
values: I,
corrected_cases: &'static [&'static str],
) -> impl Iterator<Item = (String, V)>
where
I: IntoIterator<Item = (String, V)>,
{
let case_sensitive = self.case_sensitive;
values.into_iter().map(move |(key, value)| {
if case_sensitive.not() {
if let Some(coerced_key) = corrected_cases
.iter()
.find(|item| item.eq_ignore_ascii_case(&key))
{
(coerced_key.to_string(), value)
} else {
(key, value)
}
} else {
(key, value)
}
})
}
}
#[cfg(test)]
mod tests {
use super::{Config, Value};
#[test]
fn convert_list_of_key_vals_to_tree() {
let input = vec![
(String::from("FOO"), Value::simple("BAR")),
(String::from("BAZ"), Value::simple("124")),
(String::from("NESTED__FOO"), Value::simple("true")),
(String::from("NESTED__BAZ"), Value::simple("Hello")),
];
let expected = Value::Map(vec![
(String::from("FOO"), Value::simple("BAR")),
(String::from("BAZ"), Value::simple("124")),
(
String::from("NESTED"),
Value::Map(vec![
(String::from("FOO"), Value::simple("true")),
(String::from("BAZ"), Value::simple("Hello")),
]),
),
]);
let config = Config::new();
let actual = config.create_parser(input).unwrap();
assert_eq!(actual.current, expected);
}
#[test]
fn custom_sep() {
let input = vec![
(String::from("FOO"), Value::simple("bar")),
(String::from("BAZ"), Value::simple("124")),
(String::from("NESTED#FOO"), Value::simple("true")),
(String::from("NESTED#BAZ"), Value::simple("Hello")),
];
let expected = Value::Map(vec![
(String::from("FOO"), Value::simple("bar")),
(String::from("BAZ"), Value::simple("124")),
(
String::from("NESTED"),
Value::Map(vec![
(String::from("FOO"), Value::simple("true")),
(String::from("BAZ"), Value::simple("Hello")),
]),
),
]);
let mut config = Config::new();
let actual = config.with_separator("#").create_parser(input).unwrap();
assert_eq!(actual.current, expected);
}
}