From fa51c1ed15ef683c54a25733ac915f593c9e1746 Mon Sep 17 00:00:00 2001 From: Moritz Althaus Date: Mon, 23 Dec 2024 16:10:37 +0100 Subject: [PATCH 1/4] refactor: introduce case conversion strategy --- src/env.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/env.rs b/src/env.rs index 564fdd69..7f3a33e2 100644 --- a/src/env.rs +++ b/src/env.rs @@ -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, + convert_case: 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`. @@ -90,6 +90,15 @@ pub struct Environment { source: Option>, } +/// 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), +} + impl Environment { /// Optional prefix that will limit access to the environment to only keys that /// begin with the defined prefix. @@ -118,7 +127,7 @@ 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 } @@ -270,7 +279,7 @@ impl Source for Environment { } #[cfg(feature = "convert-case")] - if let Some(convert_case) = convert_case { + if let Some(ConversionStrategy::All(convert_case)) = convert_case { key = key.to_case(*convert_case); } From 6739a712189ed20295a780a460e39612285ec654 Mon Sep 17 00:00:00 2001 From: Moritz Althaus Date: Mon, 23 Dec 2024 16:33:36 +0100 Subject: [PATCH 2/4] feat: add case conversion strategy with excluded keys --- src/env.rs | 19 +++++++++++++++++-- tests/testsuite/env.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/env.rs b/src/env.rs index 7f3a33e2..3b1d88d3 100644 --- a/src/env.rs +++ b/src/env.rs @@ -97,6 +97,8 @@ pub struct Environment { enum ConversionStrategy { /// Apply the conversion to all collected keys All(Case), + /// Exclude the specified keys from conversion + Exclude(Case, Vec), } impl Environment { @@ -131,6 +133,12 @@ impl Environment { self } + #[cfg(feature = "convert-case")] + pub fn convert_case_exclude_keys(mut self, tt: Case, keys: Vec) -> Self { + self.convert_case = Some(ConversionStrategy::Exclude(tt, keys)); + self + } + /// Optional character sequence that separates the prefix from the rest of the key pub fn prefix_separator(mut self, s: &str) -> Self { self.prefix_separator = Some(s.into()); @@ -279,8 +287,15 @@ impl Source for Environment { } #[cfg(feature = "convert-case")] - if let Some(ConversionStrategy::All(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); + } + } + } } let value = if self.try_parsing { diff --git a/tests/testsuite/env.rs b/tests/testsuite/env.rs index 00a59e8e..acf25d7e 100644 --- a/tests/testsuite/env.rs +++ b/tests/testsuite/env.rs @@ -574,6 +574,33 @@ 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, vec!["value_a".to_owned()]); + + 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` From 32b0a03494f6dc58ac525bd6013336843e715a75 Mon Sep 17 00:00:00 2001 From: Moritz Althaus Date: Mon, 23 Dec 2024 16:43:36 +0100 Subject: [PATCH 3/4] feat: add case conversion strategy for specific keys --- src/env.rs | 13 +++++++++++++ tests/testsuite/env.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/env.rs b/src/env.rs index 3b1d88d3..552eb462 100644 --- a/src/env.rs +++ b/src/env.rs @@ -99,6 +99,8 @@ enum ConversionStrategy { All(Case), /// Exclude the specified keys from conversion Exclude(Case, Vec), + /// Only convert the specified keys + Only(Case, Vec), } impl Environment { @@ -139,6 +141,12 @@ impl Environment { self } + #[cfg(feature = "convert-case")] + pub fn convert_case_for_keys(mut self, tt: Case, keys: Vec) -> Self { + self.convert_case = Some(ConversionStrategy::Only(tt, keys)); + self + } + /// Optional character sequence that separates the prefix from the rest of the key pub fn prefix_separator(mut self, s: &str) -> Self { self.prefix_separator = Some(s.into()); @@ -295,6 +303,11 @@ impl Source for Environment { key = key.to_case(*convert_case); } } + ConversionStrategy::Only(convert_case, keys) => { + if keys.contains(&key) { + key = key.to_case(*convert_case); + } + } } } diff --git a/tests/testsuite/env.rs b/tests/testsuite/env.rs index acf25d7e..2d34b5c7 100644 --- a/tests/testsuite/env.rs +++ b/tests/testsuite/env.rs @@ -601,6 +601,33 @@ fn test_parse_kebab_case_with_exclude_keys() { ); } +#[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, vec!["value_b".to_owned()]); + + 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` From a06d3b40105bc020afbc08f6d7aef69bdfd2de8f Mon Sep 17 00:00:00 2001 From: Moritz Althaus Date: Mon, 23 Dec 2024 17:05:59 +0100 Subject: [PATCH 4/4] feat: relax type expectation of case conversion strategy --- src/env.rs | 22 ++++++++++++++++++---- tests/testsuite/env.rs | 8 ++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/env.rs b/src/env.rs index 552eb462..743fb75c 100644 --- a/src/env.rs +++ b/src/env.rs @@ -136,14 +136,28 @@ impl Environment { } #[cfg(feature = "convert-case")] - pub fn convert_case_exclude_keys(mut self, tt: Case, keys: Vec) -> Self { - self.convert_case = Some(ConversionStrategy::Exclude(tt, keys)); + pub fn convert_case_exclude_keys( + mut self, + tt: Case, + keys: impl IntoIterator>, + ) -> 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: Vec) -> Self { - self.convert_case = Some(ConversionStrategy::Only(tt, keys)); + pub fn convert_case_for_keys( + mut self, + tt: Case, + keys: impl IntoIterator>, + ) -> Self { + self.convert_case = Some(ConversionStrategy::Only( + tt, + keys.into_iter().map(|k| k.into()).collect(), + )); self } diff --git a/tests/testsuite/env.rs b/tests/testsuite/env.rs index 2d34b5c7..448c8e25 100644 --- a/tests/testsuite/env.rs +++ b/tests/testsuite/env.rs @@ -588,8 +588,8 @@ fn test_parse_kebab_case_with_exclude_keys() { temp_env::with_vars( vec![("VALUE_A", Some("value1")), ("VALUE_B", Some("value2"))], || { - let environment = Environment::default() - .convert_case_exclude_keys(Case::Kebab, vec!["value_a".to_owned()]); + let environment = + Environment::default().convert_case_exclude_keys(Case::Kebab, ["value_a"]); let config = Config::builder().add_source(environment).build().unwrap(); @@ -615,8 +615,8 @@ fn test_parse_kebab_case_for_keys() { temp_env::with_vars( vec![("VALUE_A", Some("value1")), ("VALUE_B", Some("value2"))], || { - let environment = Environment::default() - .convert_case_for_keys(Case::Kebab, vec!["value_b".to_owned()]); + let environment = + Environment::default().convert_case_for_keys(Case::Kebab, ["value_b"]); let config = Config::builder().add_source(environment).build().unwrap();