Skip to content

Commit fdee30b

Browse files
feat: adding support for dotenv files
1 parent bf6e256 commit fdee30b

File tree

6 files changed

+137
-1
lines changed

6 files changed

+137
-1
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,13 @@ pre-release-replacements = [
121121
]
122122

123123
[features]
124-
default = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case", "async"]
124+
default = ["toml", "json", "yaml", "ini", "ron", "json5", "dotenv", "convert-case", "async"]
125125
json = ["serde_json"]
126126
yaml = ["yaml-rust2"]
127127
ini = ["rust-ini"]
128128
json5 = ["json5_rs", "dep:serde-untagged"]
129129
corn = ["dep:corn"]
130+
dotenv = ["dotenvy"]
130131
convert-case = ["convert_case"]
131132
preserve_order = ["indexmap", "toml?/preserve_order", "serde_json?/preserve_order", "ron?/indexmap"]
132133
async = ["async-trait"]
@@ -143,6 +144,7 @@ rust-ini = { version = "0.21.3", optional = true }
143144
ron = { version = "0.8.1", optional = true }
144145
json5_rs = { version = "0.4.1", optional = true, package = "json5" }
145146
corn = { version = "0.10.0", optional = true, package = "libcorn" }
147+
dotenvy ={ version = "0.15.7", optional = true }
146148
indexmap = { version = "2.11.4", features = ["serde"], optional = true }
147149
convert_case = { version = "0.6.0", optional = true }
148150
pathdiff = "0.2.3"

src/file/format/dotenv.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use std::error::Error;
2+
use std::io::Cursor;
3+
4+
use dotenvy::from_read_iter;
5+
6+
use crate::map::Map;
7+
use crate::value::{Value, ValueKind};
8+
9+
pub(crate) fn parse(
10+
uri: Option<&String>,
11+
text: &str,
12+
) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
13+
let mut map: Map<String, Value> = Map::new();
14+
let cursor = Cursor::new(text);
15+
16+
for item in from_read_iter(cursor) {
17+
let (key, mut value) = item?;
18+
19+
let os_env_vars = std::env::vars_os();
20+
for (os_string_key, os_string_value) in os_env_vars {
21+
let string_key: String = os_string_key.to_string_lossy().into_owned();
22+
if string_key == key {
23+
value = os_string_value.to_string_lossy().into_owned();
24+
}
25+
}
26+
27+
map.insert(key, Value::new(uri, ValueKind::String(value)));
28+
}
29+
30+
Ok(map)
31+
}

src/file/format/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ mod json5;
2424
#[cfg(feature = "corn")]
2525
mod corn;
2626

27+
#[cfg(feature = "dotenv")]
28+
mod dotenv;
29+
2730
/// File formats provided by the library.
2831
///
2932
/// Although it is possible to define custom formats using [`Format`] trait it is recommended to use `FileFormat` if possible.
@@ -57,6 +60,10 @@ pub enum FileFormat {
5760
/// Corn (parsed with `libcorn`)
5861
#[cfg(feature = "corn")]
5962
Corn,
63+
64+
/// Dotenv (parsed with `dotenvy`)
65+
#[cfg(feature = "dotenv")]
66+
Dotenv,
6067
}
6168

6269
impl FileFormat {
@@ -76,6 +83,8 @@ impl FileFormat {
7683
FileFormat::Json5,
7784
#[cfg(feature = "corn")]
7885
FileFormat::Corn,
86+
#[cfg(feature = "dotenv")]
87+
FileFormat::Dotenv,
7988
]
8089
}
8190

@@ -102,13 +111,17 @@ impl FileFormat {
102111
#[cfg(feature = "corn")]
103112
FileFormat::Corn => &["corn"],
104113

114+
#[cfg(feature = "dotenv")]
115+
FileFormat::Dotenv => &["dotenv"],
116+
105117
#[cfg(all(
106118
not(feature = "toml"),
107119
not(feature = "json"),
108120
not(feature = "yaml"),
109121
not(feature = "ini"),
110122
not(feature = "ron"),
111123
not(feature = "json5"),
124+
not(feature = "dotenv"),
112125
))]
113126
_ => unreachable!("No features are enabled, this library won't work without features"),
114127
}
@@ -141,13 +154,17 @@ impl FileFormat {
141154
#[cfg(feature = "corn")]
142155
FileFormat::Corn => corn::parse(uri, text),
143156

157+
#[cfg(feature = "dotenv")]
158+
FileFormat::Dotenv => dotenv::parse(uri, text),
159+
144160
#[cfg(all(
145161
not(feature = "toml"),
146162
not(feature = "json"),
147163
not(feature = "yaml"),
148164
not(feature = "ini"),
149165
not(feature = "ron"),
150166
not(feature = "json5"),
167+
not(feature = "dotenv"),
151168
))]
152169
_ => unreachable!("No features are enabled, this library won't work without features"),
153170
}

tests/testsuite/file_dotenv.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#![cfg(feature = "dotenv")]
2+
3+
use config::Config;
4+
use std::env;
5+
6+
#[test]
7+
fn basic_dotenv() {
8+
let s = Config::builder()
9+
.add_source(config::File::from_str(
10+
r#"
11+
FOO=bar
12+
BAZ=qux
13+
"#,
14+
config::FileFormat::Dotenv,
15+
))
16+
.build()
17+
.unwrap();
18+
19+
assert_eq!(s.get::<String>("FOO").unwrap(), "bar");
20+
assert_eq!(s.get::<String>("BAZ").unwrap(), "qux");
21+
}
22+
23+
#[test]
24+
fn optional_variables() {
25+
let s = Config::builder()
26+
.add_source(config::File::from_str(
27+
r#"
28+
FOO=bar
29+
BAZ=${FOO}
30+
BAR=${UNDEFINED:-}
31+
"#,
32+
config::FileFormat::Dotenv,
33+
))
34+
.build()
35+
.unwrap();
36+
37+
assert_eq!(s.get::<String>("BAR").unwrap(), "");
38+
}
39+
40+
#[test]
41+
fn multiple_files() {
42+
let s = Config::builder()
43+
.add_source(config::File::from_str(
44+
r#"
45+
FOO=bar
46+
"#,
47+
config::FileFormat::Dotenv,
48+
))
49+
.add_source(config::File::from_str(
50+
r#"
51+
BAZ=qux
52+
"#,
53+
config::FileFormat::Dotenv,
54+
))
55+
.build()
56+
.unwrap();
57+
58+
assert_eq!(s.get::<String>("FOO").unwrap(), "bar");
59+
assert_eq!(s.get::<String>("BAZ").unwrap(), "qux");
60+
}
61+
62+
#[test]
63+
fn environment_overrides() {
64+
env::set_var("FOOBAR", "env_value");
65+
66+
let s = Config::builder()
67+
.add_source(config::File::from_str(
68+
r#"
69+
FOOBAR=file_value
70+
"#,
71+
config::FileFormat::Dotenv,
72+
))
73+
.add_source(config::Environment::with_prefix("env").separator("_"))
74+
.build()
75+
.unwrap();
76+
77+
assert_eq!(s.get::<String>("FOOBAR").unwrap(), "env_value");
78+
}

tests/testsuite/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod file_json5;
1515
pub mod file_ron;
1616
pub mod file_toml;
1717
pub mod file_yaml;
18+
pub mod file_dotenv;
1819
pub mod get;
1920
pub mod integer_range;
2021
pub mod log;

0 commit comments

Comments
 (0)