Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

env: add a 'convert_case' field to ease dealing with kebab-case #381

Merged
merged 3 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down
31 changes: 31 additions & 0 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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>,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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>() {
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
matthiasbeyer marked this conversation as resolved.
Show resolved Hide resolved
55 changes: 55 additions & 0 deletions tests/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,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`
Expand Down