Skip to content

Commit 1ccfc0a

Browse files
Merge pull request #381 from da-x/convert-case
env: add a 'convert_case' field to ease dealing with kebab-case
2 parents cbc0559 + bc06e5e commit 1ccfc0a

File tree

4 files changed

+93
-1
lines changed

4 files changed

+93
-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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ use crate::map::Map;
55
use crate::source::Source;
66
use crate::value::{Value, ValueKind};
77

8+
#[cfg(feature = "convert-case")]
9+
use convert_case::{Case, Casing};
10+
11+
/// An environment source collects a dictionary of environment variables values into a hierarchical
12+
/// config Value type. We have to be aware how the config tree is created from the environment
13+
/// dictionary, therefore we are mindful about prefixes for the environment keys, level separators,
14+
/// encoding form (kebab, snake case) etc.
815
#[must_use]
916
#[derive(Clone, Debug, Default)]
1017
pub struct Environment {
@@ -25,6 +32,12 @@ pub struct Environment {
2532
/// an environment key of `REDIS_PASSWORD` to match.
2633
separator: Option<String>,
2734

35+
/// Optional directive to translate collected keys into a form that matches what serializers
36+
/// that the configuration would expect. For example if you have the `kebab-case` attribute
37+
/// for your serde config types, you may want to pass Case::Kebab here.
38+
#[cfg(feature = "convert-case")]
39+
convert_case: Option<convert_case::Case>,
40+
2841
/// Optional character sequence that separates each env value into a vector. only works when try_parsing is set to true
2942
/// Once set, you cannot have type String on the same environment, unless you set list_parse_keys.
3043
list_separator: Option<String>,
@@ -95,6 +108,17 @@ impl Environment {
95108
self
96109
}
97110

111+
#[cfg(feature = "convert-case")]
112+
pub fn with_convert_case(tt: Case) -> Self {
113+
Self::default().convert_case(tt)
114+
}
115+
116+
#[cfg(feature = "convert-case")]
117+
pub fn convert_case(mut self, tt: Case) -> Self {
118+
self.convert_case = Some(tt);
119+
self
120+
}
121+
98122
pub fn prefix_separator(mut self, s: &str) -> Self {
99123
self.prefix_separator = Some(s.into());
100124
self
@@ -160,6 +184,8 @@ impl Source for Environment {
160184
let uri: String = "the environment".into();
161185

162186
let separator = self.separator.as_deref().unwrap_or("");
187+
#[cfg(feature = "convert-case")]
188+
let convert_case = &self.convert_case;
163189
let prefix_separator = match (self.prefix_separator.as_deref(), self.separator.as_deref()) {
164190
(Some(pre), _) => pre,
165191
(None, Some(sep)) => sep,
@@ -198,6 +224,11 @@ impl Source for Environment {
198224
key = key.replace(separator, ".");
199225
}
200226

227+
#[cfg(feature = "convert-case")]
228+
if let Some(convert_case) = convert_case {
229+
key = key.to_case(*convert_case);
230+
}
231+
201232
let value = if self.try_parsing {
202233
// convert to lowercase because bool parsing expects all lowercase
203234
if let Ok(parsed) = value.to_lowercase().parse::<bool>() {

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,7 @@ 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+
#[cfg(feature = "convert-case")]
48+
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)