Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add case conversion strategies to include/exclude keys #626

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
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: 56 additions & 5 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ pub struct Environment {

/// Optional directive to translate collected keys into a form that matches what serializers
/// that the configuration would expect. For example if you have the `kebab-case` attribute
/// for your serde config types, you may want to pass `Case::Kebab` here.
/// for your serde config types, you may want to pass `ConversionStrategy::All(Case::Kebab)` here.
#[cfg(feature = "convert-case")]
convert_case: Option<Case>,
convert_case: Option<ConversionStrategy>,

/// 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`.
Expand Down Expand Up @@ -90,6 +90,19 @@ pub struct Environment {
source: Option<Map<String, String>>,
}

/// Strategy to translate collected keys into a form that matches what serializers
/// that the configuration would expect.
#[cfg(feature = "convert-case")]
#[derive(Clone, Debug)]
enum ConversionStrategy {
/// Apply the conversion to all collected keys
All(Case),
/// Exclude the specified keys from conversion
Exclude(Case, Vec<String>),
/// Only convert the specified keys
Only(Case, Vec<String>),
}

impl Environment {
/// Optional prefix that will limit access to the environment to only keys that
/// begin with the defined prefix.
Expand Down Expand Up @@ -118,7 +131,33 @@ impl Environment {

#[cfg(feature = "convert-case")]
pub fn convert_case(mut self, tt: Case) -> Self {
self.convert_case = Some(tt);
self.convert_case = Some(ConversionStrategy::All(tt));
self
}

#[cfg(feature = "convert-case")]
pub fn convert_case_exclude_keys(
mut self,
tt: Case,
keys: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.convert_case = Some(ConversionStrategy::Exclude(
tt,
keys.into_iter().map(|k| k.into()).collect(),
));
self
}

#[cfg(feature = "convert-case")]
pub fn convert_case_for_keys(
mut self,
tt: Case,
keys: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.convert_case = Some(ConversionStrategy::Only(
tt,
keys.into_iter().map(|k| k.into()).collect(),
));
self
}

Expand Down Expand Up @@ -270,8 +309,20 @@ impl Source for Environment {
}

#[cfg(feature = "convert-case")]
if let Some(convert_case) = convert_case {
key = key.to_case(*convert_case);
if let Some(strategy) = convert_case {
match strategy {
ConversionStrategy::All(convert_case) => key = key.to_case(*convert_case),
ConversionStrategy::Exclude(convert_case, keys) => {
if !keys.contains(&key) {
key = key.to_case(*convert_case);
}
}
ConversionStrategy::Only(convert_case, keys) => {
if keys.contains(&key) {
key = key.to_case(*convert_case);
}
}
}
}

let value = if self.try_parsing {
Expand Down
54 changes: 54 additions & 0 deletions tests/testsuite/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,60 @@ fn test_parse_nested_kebab() {
);
}

#[test]
#[cfg(feature = "convert-case")]
fn test_parse_kebab_case_with_exclude_keys() {
use config::Case;
#[derive(Deserialize, Debug)]
struct TestConfig {
value_a: String,
#[serde(rename = "value-b")]
value_b: String,
}

temp_env::with_vars(
vec![("VALUE_A", Some("value1")), ("VALUE_B", Some("value2"))],
|| {
let environment =
Environment::default().convert_case_exclude_keys(Case::Kebab, ["value_a"]);

let config = Config::builder().add_source(environment).build().unwrap();

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

assert_eq!(config.value_a, "value1");
assert_eq!(config.value_b, "value2");
},
);
}

#[test]
#[cfg(feature = "convert-case")]
fn test_parse_kebab_case_for_keys() {
use config::Case;
#[derive(Deserialize, Debug)]
struct TestConfig {
value_a: String,
#[serde(rename = "value-b")]
value_b: String,
}

temp_env::with_vars(
vec![("VALUE_A", Some("value1")), ("VALUE_B", Some("value2"))],
|| {
let environment =
Environment::default().convert_case_for_keys(Case::Kebab, ["value_b"]);

let config = Config::builder().add_source(environment).build().unwrap();

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

assert_eq!(config.value_a, "value1");
assert_eq!(config.value_b, "value2");
},
);
}

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