From e3167a16910fdf65aa903e7689e84011de6563e3 Mon Sep 17 00:00:00 2001 From: Dan Aloni Date: Thu, 29 Sep 2022 15:58:38 +0300 Subject: [PATCH 1/3] Environment: add a rustdoc comment --- src/env.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/env.rs b/src/env.rs index 432df2c8..683459ab 100644 --- a/src/env.rs +++ b/src/env.rs @@ -5,6 +5,10 @@ use crate::map::Map; use crate::source::Source; use crate::value::{Value, ValueKind}; +/// An environment source collects a dictionary of environment variables values into a hierarchical +/// config Value type. We have to be aware how the config tree is created from the environment +/// dictionary, therefore we are mindful about prefixes for the environment keys, level separators, +/// encoding form (kebab, snake case) etc. #[must_use] #[derive(Clone, Debug, Default)] pub struct Environment { From f8e577f299370ed4b9a0b5a352f42eb899f86c01 Mon Sep 17 00:00:00 2001 From: Dan Aloni Date: Wed, 28 Sep 2022 18:33:46 +0300 Subject: [PATCH 2/3] env: add a 'convert_case' field to ease dealing with kebab-case This allows usage of `kebab-case` attribute in serde, mapping unambiguously into a config value given a multiple character separator. This also add the `convert-case` feature. For example: let environment = Environment::default() .prefix("PREFIX") .translate_key(Case::Kebab) .separator("__"); --- Cargo.toml | 4 +++- src/env.rs | 20 +++++++++++++++++++ src/lib.rs | 3 +++ tests/env.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5f6f705f..34e5532a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,12 @@ edition = "2018" maintenance = { status = "actively-developed" } [features] -default = ["toml", "json", "yaml", "ini", "ron", "json5"] +default = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case"] json = ["serde_json"] yaml = ["yaml-rust"] ini = ["rust-ini"] json5 = ["json5_rs"] +convert-case = ["convert_case"] preserve_order = ["indexmap", "toml/preserve_order", "serde_json/preserve_order", "ron/indexmap"] [dependencies] @@ -35,6 +36,7 @@ rust-ini = { version = "0.18", optional = true } ron = { version = "0.8", optional = true } json5_rs = { version = "0.4", optional = true, package = "json5" } indexmap = { version = "1.7.0", features = ["serde-1"], optional = true} +convert_case = { version = "0.6", optional = true } pathdiff = "0.2" [dev-dependencies] diff --git a/src/env.rs b/src/env.rs index 683459ab..e07ac9ba 100644 --- a/src/env.rs +++ b/src/env.rs @@ -5,6 +5,8 @@ use crate::map::Map; use crate::source::Source; use crate::value::{Value, ValueKind}; +use convert_case::{Case, Casing}; + /// An environment source collects a dictionary of environment variables values into a hierarchical /// config Value type. We have to be aware how the config tree is created from the environment /// dictionary, therefore we are mindful about prefixes for the environment keys, level separators, @@ -29,6 +31,11 @@ pub struct Environment { /// an environment key of `REDIS_PASSWORD` to match. separator: Option, + /// 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. + 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. list_separator: Option, @@ -99,6 +106,15 @@ impl Environment { self } + pub fn with_convert_case(tt: Case) -> Self { + Self::default().convert_case(tt) + } + + pub fn convert_case(mut self, tt: Case) -> Self { + self.convert_case = Some(tt); + self + } + pub fn prefix_separator(mut self, s: &str) -> Self { self.prefix_separator = Some(s.into()); self @@ -164,6 +180,7 @@ impl Source for Environment { let uri: String = "the environment".into(); let separator = self.separator.as_deref().unwrap_or(""); + let convert_case = &self.convert_case; let prefix_separator = match (self.prefix_separator.as_deref(), self.separator.as_deref()) { (Some(pre), _) => pre, (None, Some(sep)) => sep, @@ -201,6 +218,9 @@ impl Source for Environment { if !separator.is_empty() { key = key.replace(separator, "."); } + if let Some(convert_case) = convert_case { + key = key.to_case(*convert_case); + } let value = if self.try_parsing { // convert to lowercase because bool parsing expects all lowercase diff --git a/src/lib.rs b/src/lib.rs index 589d2d51..b5da67a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,3 +42,6 @@ pub use crate::format::Format; pub use crate::map::Map; pub use crate::source::{AsyncSource, Source}; pub use crate::value::{Value, ValueKind}; + +// Re-export +pub use convert_case::Case; diff --git a/tests/env.rs b/tests/env.rs index 3a24bde5..a144d080 100644 --- a/tests/env.rs +++ b/tests/env.rs @@ -463,6 +463,61 @@ fn test_parse_string_and_list() { ) } +#[test] +fn test_parse_nested_kebab() { + use config::Case; + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "kebab-case")] + struct TestConfig { + single: String, + plain: SimpleInner, + value_with_multipart_name: String, + inner_config: ComplexInner, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "kebab-case")] + struct SimpleInner { + val: String, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "kebab-case")] + struct ComplexInner { + another_multipart_name: String, + } + + temp_env::with_vars( + vec![ + ("PREFIX__SINGLE", Some("test")), + ("PREFIX__PLAIN__VAL", Some("simple")), + ("PREFIX__VALUE_WITH_MULTIPART_NAME", Some("value1")), + ( + "PREFIX__INNER_CONFIG__ANOTHER_MULTIPART_NAME", + Some("value2"), + ), + ], + || { + let environment = Environment::default() + .prefix("PREFIX") + .convert_case(Case::Kebab) + .separator("__"); + + let config = Config::builder().add_source(environment).build().unwrap(); + + println!("{:#?}", config); + + let config: TestConfig = config.try_deserialize().unwrap(); + + assert_eq!(config.single, "test"); + assert_eq!(config.plain.val, "simple"); + assert_eq!(config.value_with_multipart_name, "value1"); + assert_eq!(config.inner_config.another_multipart_name, "value2"); + }, + ) +} + #[test] fn test_parse_string() { // using a struct in an enum here to make serde use `deserialize_any` From bc06e5e353c87549e35e48adef2d11cea8d675c9 Mon Sep 17 00:00:00 2001 From: Dan Aloni Date: Thu, 29 Sep 2022 16:36:31 +0300 Subject: [PATCH 3/3] convert-case: fix for building with this feature excluded --- src/env.rs | 7 +++++++ src/lib.rs | 1 + 2 files changed, 8 insertions(+) diff --git a/src/env.rs b/src/env.rs index e07ac9ba..93029034 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}; +#[cfg(feature = "convert-case")] use convert_case::{Case, Casing}; /// An environment source collects a dictionary of environment variables values into a hierarchical @@ -34,6 +35,7 @@ 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. + #[cfg(feature = "convert-case")] convert_case: Option, /// Optional character sequence that separates each env value into a vector. only works when try_parsing is set to true @@ -106,10 +108,12 @@ impl Environment { self } + #[cfg(feature = "convert-case")] pub fn with_convert_case(tt: Case) -> Self { Self::default().convert_case(tt) } + #[cfg(feature = "convert-case")] pub fn convert_case(mut self, tt: Case) -> Self { self.convert_case = Some(tt); self @@ -180,6 +184,7 @@ impl Source for Environment { let uri: String = "the environment".into(); let separator = self.separator.as_deref().unwrap_or(""); + #[cfg(feature = "convert-case")] let convert_case = &self.convert_case; let prefix_separator = match (self.prefix_separator.as_deref(), self.separator.as_deref()) { (Some(pre), _) => pre, @@ -218,6 +223,8 @@ impl Source for Environment { if !separator.is_empty() { key = key.replace(separator, "."); } + + #[cfg(feature = "convert-case")] if let Some(convert_case) = convert_case { key = key.to_case(*convert_case); } diff --git a/src/lib.rs b/src/lib.rs index b5da67a9..41bf549a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,4 +44,5 @@ pub use crate::source::{AsyncSource, Source}; pub use crate::value::{Value, ValueKind}; // Re-export +#[cfg(feature = "convert-case")] pub use convert_case::Case;