Repository: mehcode/config-rs (rust)

Issue:
environment variables list?
is it possible to parse an env var into something like `Vec<String>`? I get this error for any formatting I try:

```
thread 'main' panicked at 'somethings wrong with the settings: invalid type: string "1,2,3", expected a sequence', src/libcore/result.rs:999:5
```

the error to me suggest that it is possible, but I could just be reading things wrong.

Pull request:
feat: env contains list of strings
Signed-off-by: simon-an <26556185+simon-an@users.noreply.github.com>

fixes #117 

Patch:
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<String>,
+}
+
+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<String>,
 
+    /// 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<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>>,
+
     /// 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<String>`] instead of [`String`].
+    /// See [`with_list_parse_key`] when you want to use [`Vec<String>`] 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<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_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<Map<String, String>>) -> Self {
         self.source = source;
         self
@@ -173,6 +196,24 @@ impl Source for Environment {
                     ValueKind::I64(parsed)
                 } else if let Ok(parsed) = value.parse::<f64>() {
                     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> = 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> = value
+                            .split(separator)
+                            .map(|s| Value::new(Some(&uri), ValueKind::String(s.to_string())))
+                            .collect();
+                        ValueKind::Array(v)
+                    }
                 } else {
                     ValueKind::String(value)
                 }


Test patch:
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::<TestBoolEnum>().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<String>,
+    }
+
+    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<String>,
+    }
+
+    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`