diff --git a/src/env.rs b/src/env.rs index 7da8c937..76a0b110 100644 --- a/src/env.rs +++ b/src/env.rs @@ -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}; @@ -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 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, @@ -48,6 +69,8 @@ pub struct Environment { list_separator: Option, /// A list of keys which should always be parsed as a list. If not set you can have only `Vec` or `String` (not both) in one environment. list_parse_keys: Option>, + /// Functions used to determine if a key should be parsed as a list. If not set you can have only `Vec` or `String` (not both) in one environment. + list_parse_fn: Option, /// Ignore empty env values (treat as unset). ignore_empty: bool, @@ -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`]. + /// To switch the default type back to type Strings you need to provide the keys which should be [`Vec`] using this function. + pub fn with_list_parse_fn(mut self, check_fn: Box 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; @@ -299,22 +333,29 @@ impl Source for Environment { } else if let Ok(parsed) = value.parse::() { ValueKind::Float(parsed) } else if let Some(separator) = &self.list_separator { + let convert_to_array_fn = |v: String| -> Vec { + 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 - .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 - .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) diff --git a/tests/testsuite/env.rs b/tests/testsuite/env.rs index f8ef2b23..c1f9ee61 100644 --- a/tests/testsuite/env.rs +++ b/tests/testsuite/env.rs @@ -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, + } + + 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`