Skip to content

Commit f8e577f

Browse files
committed
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("__");
1 parent e3167a1 commit f8e577f

File tree

4 files changed

+81
-1
lines changed

4 files changed

+81
-1
lines changed

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ edition = "2018"
1515
maintenance = { status = "actively-developed" }
1616

1717
[features]
18-
default = ["toml", "json", "yaml", "ini", "ron", "json5"]
18+
default = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case"]
1919
json = ["serde_json"]
2020
yaml = ["yaml-rust"]
2121
ini = ["rust-ini"]
2222
json5 = ["json5_rs"]
23+
convert-case = ["convert_case"]
2324
preserve_order = ["indexmap", "toml/preserve_order", "serde_json/preserve_order", "ron/indexmap"]
2425

2526
[dependencies]
@@ -35,6 +36,7 @@ rust-ini = { version = "0.18", optional = true }
3536
ron = { version = "0.8", optional = true }
3637
json5_rs = { version = "0.4", optional = true, package = "json5" }
3738
indexmap = { version = "1.7.0", features = ["serde-1"], optional = true}
39+
convert_case = { version = "0.6", optional = true }
3840
pathdiff = "0.2"
3941

4042
[dev-dependencies]

src/env.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use crate::map::Map;
55
use crate::source::Source;
66
use crate::value::{Value, ValueKind};
77

8+
use convert_case::{Case, Casing};
9+
810
/// An environment source collects a dictionary of environment variables values into a hierarchical
911
/// config Value type. We have to be aware how the config tree is created from the environment
1012
/// dictionary, therefore we are mindful about prefixes for the environment keys, level separators,
@@ -29,6 +31,11 @@ pub struct Environment {
2931
/// an environment key of `REDIS_PASSWORD` to match.
3032
separator: Option<String>,
3133

34+
/// Optional directive to translate collected keys into a form that matches what serializers
35+
/// that the configuration would expect. For example if you have the `kebab-case` attribute
36+
/// for your serde config types, you may want to pass Case::Kebab here.
37+
convert_case: Option<convert_case::Case>,
38+
3239
/// Optional character sequence that separates each env value into a vector. only works when try_parsing is set to true
3340
/// Once set, you cannot have type String on the same environment, unless you set list_parse_keys.
3441
list_separator: Option<String>,
@@ -99,6 +106,15 @@ impl Environment {
99106
self
100107
}
101108

109+
pub fn with_convert_case(tt: Case) -> Self {
110+
Self::default().convert_case(tt)
111+
}
112+
113+
pub fn convert_case(mut self, tt: Case) -> Self {
114+
self.convert_case = Some(tt);
115+
self
116+
}
117+
102118
pub fn prefix_separator(mut self, s: &str) -> Self {
103119
self.prefix_separator = Some(s.into());
104120
self
@@ -164,6 +180,7 @@ impl Source for Environment {
164180
let uri: String = "the environment".into();
165181

166182
let separator = self.separator.as_deref().unwrap_or("");
183+
let convert_case = &self.convert_case;
167184
let prefix_separator = match (self.prefix_separator.as_deref(), self.separator.as_deref()) {
168185
(Some(pre), _) => pre,
169186
(None, Some(sep)) => sep,
@@ -201,6 +218,9 @@ impl Source for Environment {
201218
if !separator.is_empty() {
202219
key = key.replace(separator, ".");
203220
}
221+
if let Some(convert_case) = convert_case {
222+
key = key.to_case(*convert_case);
223+
}
204224

205225
let value = if self.try_parsing {
206226
// convert to lowercase because bool parsing expects all lowercase

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ pub use crate::format::Format;
4242
pub use crate::map::Map;
4343
pub use crate::source::{AsyncSource, Source};
4444
pub use crate::value::{Value, ValueKind};
45+
46+
// Re-export
47+
pub use convert_case::Case;

tests/env.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,61 @@ fn test_parse_string_and_list() {
463463
)
464464
}
465465

466+
#[test]
467+
fn test_parse_nested_kebab() {
468+
use config::Case;
469+
470+
#[derive(Deserialize, Debug)]
471+
#[serde(rename_all = "kebab-case")]
472+
struct TestConfig {
473+
single: String,
474+
plain: SimpleInner,
475+
value_with_multipart_name: String,
476+
inner_config: ComplexInner,
477+
}
478+
479+
#[derive(Deserialize, Debug)]
480+
#[serde(rename_all = "kebab-case")]
481+
struct SimpleInner {
482+
val: String,
483+
}
484+
485+
#[derive(Deserialize, Debug)]
486+
#[serde(rename_all = "kebab-case")]
487+
struct ComplexInner {
488+
another_multipart_name: String,
489+
}
490+
491+
temp_env::with_vars(
492+
vec![
493+
("PREFIX__SINGLE", Some("test")),
494+
("PREFIX__PLAIN__VAL", Some("simple")),
495+
("PREFIX__VALUE_WITH_MULTIPART_NAME", Some("value1")),
496+
(
497+
"PREFIX__INNER_CONFIG__ANOTHER_MULTIPART_NAME",
498+
Some("value2"),
499+
),
500+
],
501+
|| {
502+
let environment = Environment::default()
503+
.prefix("PREFIX")
504+
.convert_case(Case::Kebab)
505+
.separator("__");
506+
507+
let config = Config::builder().add_source(environment).build().unwrap();
508+
509+
println!("{:#?}", config);
510+
511+
let config: TestConfig = config.try_deserialize().unwrap();
512+
513+
assert_eq!(config.single, "test");
514+
assert_eq!(config.plain.val, "simple");
515+
assert_eq!(config.value_with_multipart_name, "value1");
516+
assert_eq!(config.inner_config.another_multipart_name, "value2");
517+
},
518+
)
519+
}
520+
466521
#[test]
467522
fn test_parse_string() {
468523
// using a struct in an enum here to make serde use `deserialize_any`

0 commit comments

Comments
 (0)