diff options
author | Matthias Beyer <mail@beyermatthias.de> | 2022-10-10 08:23:15 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-10 08:23:15 +0200 |
commit | 1ccfc0a685d21988ec5a955d3e725c22c2bfc9db (patch) | |
tree | bb79f69c6e74ab0752bb861172b99f18d9e74fa7 | |
parent | cbc05597835c4b87e6792b067e609ca54aead334 (diff) | |
parent | bc06e5e353c87549e35e48adef2d11cea8d675c9 (diff) |
Merge pull request #381 from da-x/convert-case
env: add a 'convert_case' field to ease dealing with kebab-case
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | src/env.rs | 31 | ||||
-rw-r--r-- | src/lib.rs | 4 | ||||
-rw-r--r-- | tests/env.rs | 55 |
4 files changed, 93 insertions, 1 deletions
@@ -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] @@ -5,6 +5,13 @@ 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 +/// 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 { @@ -25,6 +32,12 @@ pub struct Environment { /// an environment key of `REDIS_PASSWORD` to match. separator: Option<String>, + /// 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<convert_case::Case>, + /// 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>, @@ -95,6 +108,17 @@ 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 + } + pub fn prefix_separator(mut self, s: &str) -> Self { self.prefix_separator = Some(s.into()); self @@ -160,6 +184,8 @@ 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, (None, Some(sep)) => sep, @@ -198,6 +224,11 @@ impl Source for Environment { key = key.replace(separator, "."); } + #[cfg(feature = "convert-case")] + 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 if let Ok(parsed) = value.to_lowercase().parse::<bool>() { @@ -42,3 +42,7 @@ pub use crate::format::Format; pub use crate::map::Map; pub use crate::source::{AsyncSource, Source}; pub use crate::value::{Value, ValueKind}; + +// Re-export +#[cfg(feature = "convert-case")] +pub use convert_case::Case; diff --git a/tests/env.rs b/tests/env.rs index 3a24bde..a144d08 100644 --- a/tests/env.rs +++ b/tests/env.rs @@ -464,6 +464,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` #[derive(Deserialize, Debug)] |