summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2022-10-10 08:23:15 +0200
committerGitHub <noreply@github.com>2022-10-10 08:23:15 +0200
commit1ccfc0a685d21988ec5a955d3e725c22c2bfc9db (patch)
treebb79f69c6e74ab0752bb861172b99f18d9e74fa7
parentcbc05597835c4b87e6792b067e609ca54aead334 (diff)
parentbc06e5e353c87549e35e48adef2d11cea8d675c9 (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.toml4
-rw-r--r--src/env.rs31
-rw-r--r--src/lib.rs4
-rw-r--r--tests/env.rs55
4 files changed, 93 insertions, 1 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 5f6f705..34e5532 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 432df2c..9302903 100644
--- a/src/env.rs
+++ b/src/env.rs
@@ -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>() {
diff --git a/src/lib.rs b/src/lib.rs
index 589d2d5..41bf549 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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)]