Skip to content

Commit

Permalink
Merge pull request #255 from simon-an/parse-environment-list
Browse files Browse the repository at this point in the history
feat: env contains list of strings
  • Loading branch information
matthiasbeyer authored Mar 2, 2022
2 parents 7951a75 + 53322d4 commit 2e9ccf7
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 6 deletions.
25 changes: 25 additions & 0 deletions examples/env-list/main.rs
Original file line number Diff line number Diff line change
@@ -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");
}
53 changes: 47 additions & 6 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,

Expand Down Expand Up @@ -80,39 +87,55 @@ 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
}

/// 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
Expand Down Expand Up @@ -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)
}
Expand Down
86 changes: 86 additions & 0 deletions tests/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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`
Expand Down

0 comments on commit 2e9ccf7

Please sign in to comment.