From 53322d4ca277a2e55a7efeb654e400e5c05eb672 Mon Sep 17 00:00:00 2001 From: simon-an <26556185+simon-an@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:14:36 +0100 Subject: feat: env contains list of strings Signed-off-by: simon-an <26556185+simon-an@users.noreply.github.com> --- examples/env-list/main.rs | 25 ++++++++++++++ src/env.rs | 53 +++++++++++++++++++++++++---- tests/env.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 examples/env-list/main.rs diff --git a/examples/env-list/main.rs b/examples/env-list/main.rs new file mode 100644 index 0000000..f567419 --- /dev/null +++ b/examples/env-list/main.rs @@ -0,0 +1,25 @@ +use config::Config; +#[derive(Debug, Default, serde_derive::Deserialize, PartialEq)] +struct AppConfig { + list: Vec, +} + +fn main() { + std::env::set_var("APP_LIST", "Hello World"); + + let config = Config::builder() + .add_source( + config::Environment::with_prefix("APP") + .try_parsing(true) + .separator("_") + .list_separator(" "), + ) + .build() + .unwrap(); + + let app: AppConfig = config.try_deserialize().unwrap(); + + assert_eq!(app.list, vec![String::from("Hello"), String::from("World")]); + + std::env::remove_var("APP_LIST"); +} diff --git a/src/env.rs b/src/env.rs index 9968106..03d5785 100644 --- a/src/env.rs +++ b/src/env.rs @@ -5,6 +5,7 @@ use crate::map::Map; use crate::source::Source; use crate::value::{Value, ValueKind}; +#[must_use] #[derive(Clone, Debug, Default)] pub struct Environment { /// Optional prefix that will limit access to the environment to only keys that @@ -24,6 +25,12 @@ pub struct Environment { /// an environment key of `REDIS_PASSWORD` to match. separator: Option, + /// Optional character sequence that separates each env value into a vector. only works when try_parsing is set to true + /// Once set, you cannot have type String on the same environment, unless you set list_parse_keys. + 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>, + /// Ignore empty env values (treat as unset). ignore_empty: bool, @@ -80,25 +87,43 @@ impl Environment { } } - #[must_use] pub fn prefix(mut self, s: &str) -> Self { self.prefix = Some(s.into()); self } - #[must_use] pub fn prefix_separator(mut self, s: &str) -> Self { self.prefix_separator = Some(s.into()); self } - #[must_use] pub fn separator(mut self, s: &str) -> Self { self.separator = Some(s.into()); self } - #[must_use] + /// When set and try_parsing is true, then all environment variables will be parsed as [`Vec`] instead of [`String`]. + /// See [`with_list_parse_key`] when you want to use [`Vec`] in combination with [`String`]. + pub fn list_separator(mut self, s: &str) -> Self { + self.list_separator = Some(s.into()); + self + } + + /// Add a key which 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_key(mut self, key: &str) -> Self { + if self.list_parse_keys == None { + self.list_parse_keys = Some(vec![key.into()]) + } else { + self.list_parse_keys = self.list_parse_keys.map(|mut keys| { + keys.push(key.into()); + keys + }); + } + self + } + pub fn ignore_empty(mut self, ignore: bool) -> Self { self.ignore_empty = ignore; self @@ -106,13 +131,11 @@ impl Environment { /// Note: enabling `try_parsing` can reduce performance it will try and parse /// each environment variable 3 times (bool, i64, f64) - #[must_use] pub fn try_parsing(mut self, try_parsing: bool) -> Self { self.try_parsing = try_parsing; self } - #[must_use] pub fn source(mut self, source: Option>) -> Self { self.source = source; self @@ -173,6 +196,24 @@ impl Source for Environment { ValueKind::I64(parsed) } else if let Ok(parsed) = value.parse::() { ValueKind::Float(parsed) + } else if let Some(separator) = &self.list_separator { + 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_string()))) + .collect(); + ValueKind::Array(v) + } else { + ValueKind::String(value) + } + } else { + let v: Vec = value + .split(separator) + .map(|s| Value::new(Some(&uri), ValueKind::String(s.to_string()))) + .collect(); + ValueKind::Array(v) + } } else { ValueKind::String(value) } diff --git a/tests/env.rs b/tests/env.rs index 90852e0..fcadf81 100644 --- a/tests/env.rs +++ b/tests/env.rs @@ -392,6 +392,56 @@ fn test_parse_bool_fail() { config.try_deserialize::().unwrap(); } +#[test] +fn test_parse_string_and_list() { + // 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, + } + + env::set_var("LIST_STRING_LIST", "test,string"); + env::set_var("LIST_STRING_VAL", "test,string"); + + let environment = Environment::default() + .prefix("LIST") + .list_separator(",") + .with_list_parse_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 + ); + } + } + + env::remove_var("LIST_STRING_VAL"); + env::remove_var("LIST_STRING_LIST"); +} + #[test] fn test_parse_string() { // using a struct in an enum here to make serde use `deserialize_any` @@ -428,6 +478,42 @@ fn test_parse_string() { env::remove_var("STRING_VAL"); } +#[test] +fn test_parse_string_list() { + // using a struct in an enum here to make serde use `deserialize_any` + #[derive(Deserialize, Debug)] + #[serde(tag = "tag")] + enum TestListEnum { + StringList(TestList), + } + + #[derive(Deserialize, Debug)] + struct TestList { + string_list: Vec, + } + + env::set_var("STRING_LIST", "test string"); + + let environment = Environment::default().try_parsing(true).list_separator(" "); + + let config = Config::builder() + .set_default("tag", "StringList") + .unwrap() + .add_source(environment) + .build() + .unwrap(); + + let config: TestListEnum = config.try_deserialize().unwrap(); + + let test_string = vec![String::from("test"), String::from("string")]; + + match config { + TestListEnum::StringList(TestList { string_list }) => assert_eq!(test_string, string_list), + } + + env::remove_var("STRING_LIST"); +} + #[test] fn test_parse_off_string() { // using a struct in an enum here to make serde use `deserialize_any` -- cgit v1.2.3