Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 51 additions & 10 deletions src/env.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use core::fmt;
use std::env;
use std::ffi::OsString;
use std::sync::Arc;

#[cfg(feature = "convert-case")]
use convert_case::{Case, Casing};
Expand All @@ -10,6 +12,25 @@ use crate::source::Source;
use crate::value::{Value, ValueKind};
use crate::ConfigError;

/// Functions used to determine if a key should be parsed as a list.
struct ListParseFn {
list_parse_fn: Vec<Arc<dyn Fn(&str) -> bool + Send + Sync>>,
}

impl fmt::Debug for ListParseFn {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
Ok(())
}
}

impl Clone for ListParseFn {
fn clone(&self) -> Self {
Self {
list_parse_fn: self.list_parse_fn.clone(),
}
}
}

/// An environment source collects a dictionary of environment variables values into a hierarchical
/// config Value type. We have to be aware how the config tree is created from the environment
/// dictionary, therefore we are mindful about prefixes for the environment keys, level separators,
Expand Down Expand Up @@ -48,6 +69,8 @@ pub struct Environment {
list_separator: Option<String>,
/// 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.
list_parse_keys: Option<Vec<String>>,
/// Functions used to determine if a key should be parsed as a list. If not set you can have only `Vec<String>` or `String` (not both) in one environment.
list_parse_fn: Option<ListParseFn>,

/// Ignore empty env values (treat as unset).
ignore_empty: bool,
Expand Down Expand Up @@ -162,6 +185,17 @@ impl Environment {
self
}

/// Add a function which determined if a key should be parsed as a list when collecting [`Value`]s from the environment.
/// Once `list_separator` is set, the type for string is [`Vec<String>`].
/// To switch the default type back to type Strings you need to provide the keys which should be [`Vec<String>`] using this function.
pub fn with_list_parse_fn(mut self, check_fn: Box<dyn Fn(&str) -> bool + Send + Sync>) -> Self {
let fns = self.list_parse_fn.get_or_insert_with(|| ListParseFn {
list_parse_fn: Vec::new(),
});
fns.list_parse_fn.push(Arc::from(check_fn));
self
}

/// Ignore empty env values (treat as unset).
pub fn ignore_empty(mut self, ignore: bool) -> Self {
self.ignore_empty = ignore;
Expand Down Expand Up @@ -299,22 +333,29 @@ impl Source for Environment {
} else if let Ok(parsed) = value.parse::<f64>() {
ValueKind::Float(parsed)
} else if let Some(separator) = &self.list_separator {
let convert_to_array_fn = |v: String| -> Vec<Value> {
v.split(separator)
.map(|s| Value::new(Some(&uri), ValueKind::String(s.to_owned())))
.collect()
};
if let Some(keys) = &self.list_parse_keys {
if keys.contains(&key) {
let v: Vec<Value> = value
.split(separator)
.map(|s| Value::new(Some(&uri), ValueKind::String(s.to_owned())))
.collect();
ValueKind::Array(v)
ValueKind::Array(convert_to_array_fn(value))
} else {
ValueKind::String(value)
}
} else if let Some(list_parse_fn) = &self.list_parse_fn {
let is_matched = list_parse_fn
.list_parse_fn
.iter()
.any(|parse_fn| parse_fn(&key));
if is_matched {
ValueKind::Array(convert_to_array_fn(value))
} else {
ValueKind::String(value)
}
} else {
let v: Vec<Value> = value
.split(separator)
.map(|s| Value::new(Some(&uri), ValueKind::String(s.to_owned())))
.collect();
ValueKind::Array(v)
ValueKind::Array(convert_to_array_fn(value))
}
} else {
ValueKind::String(value)
Expand Down
52 changes: 52 additions & 0 deletions tests/testsuite/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,58 @@ fn test_parse_string_and_list() {
);
}

#[test]
fn test_parse_string_and_list_parse_fn() {
// using a struct in an enum here to make serde use `deserialize_any`
#[derive(Deserialize, Debug)]
#[serde(tag = "tag")]
enum TestStringEnum {
String(TestString),
}

#[derive(Deserialize, Debug)]
struct TestString {
string_val: String,
string_list: Vec<String>,
}

temp_env::with_vars(
vec![
("LIST_STRING_LIST", Some("test,string")),
("LIST_STRING_VAL", Some("test,string")),
],
|| {
let environment = Environment::default()
.prefix("LIST")
.list_separator(",")
.with_list_parse_fn(Box::new(|key: &str| key == "string_list"))
.try_parsing(true);

let config = Config::builder()
.set_default("tag", "String")
.unwrap()
.add_source(environment)
.build()
.unwrap();

let config: TestStringEnum = config.try_deserialize().unwrap();

match config {
TestStringEnum::String(TestString {
string_val,
string_list,
}) => {
assert_eq!(String::from("test,string"), string_val);
assert_eq!(
vec![String::from("test"), String::from("string")],
string_list
);
}
}
},
);
}

#[test]
fn test_parse_string_and_list_ignore_list_parse_key_case() {
// using a struct in an enum here to make serde use `deserialize_any`
Expand Down