diff options
author | Dan Aloni <dan@kernelim.com> | 2022-09-28 18:33:46 +0300 |
---|---|---|
committer | Dan Aloni <dan@kernelim.com> | 2022-09-29 16:07:35 +0300 |
commit | f8e577f299370ed4b9a0b5a352f42eb899f86c01 (patch) | |
tree | 1a50b5cf37063991cf9e2a860231f9c1b022e24a | |
parent | e3167a16910fdf65aa903e7689e84011de6563e3 (diff) |
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("__");
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | src/env.rs | 20 | ||||
-rw-r--r-- | src/lib.rs | 3 | ||||
-rw-r--r-- | tests/env.rs | 55 |
4 files changed, 81 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,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<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. + 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>, @@ -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 @@ -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 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)] |