From 1f7f1175d6bdf6079e2513365278c63d64f87874 Mon Sep 17 00:00:00 2001 From: chickencoding123 <18017298+chickencoding123@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:37:33 +0200 Subject: [PATCH] feat(dotenv): Adding support for dotenv files --- Cargo.lock | 7 +++ Cargo.toml | 2 + src/file/format/dotenv.rs | 31 ++++++++++++++ src/file/format/mod.rs | 17 ++++++++ tests/testsuite/file_dotenv.rs | 78 ++++++++++++++++++++++++++++++++++ tests/testsuite/main.rs | 1 + 6 files changed, 136 insertions(+) create mode 100644 src/file/format/dotenv.rs create mode 100644 tests/testsuite/file_dotenv.rs diff --git a/Cargo.lock b/Cargo.lock index 544e8dc1..a45df6cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,6 +211,7 @@ dependencies = [ "async-trait", "chrono", "convert_case", + "dotenvy", "float-cmp", "futures", "glob", @@ -326,6 +327,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "encoding_rs" version = "0.8.35" diff --git a/Cargo.toml b/Cargo.toml index e3d5e981..4888a316 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,6 +127,7 @@ yaml = ["yaml-rust2"] ini = ["rust-ini"] json5 = ["json5_rs", "dep:serde-untagged"] corn = ["dep:corn"] +dotenv = ["dep:dotenvy"] convert-case = ["convert_case"] preserve_order = ["indexmap", "toml?/preserve_order", "serde_json?/preserve_order", "ron?/indexmap"] async = ["async-trait"] @@ -143,6 +144,7 @@ rust-ini = { version = "0.21.3", optional = true } ron = { version = "0.8.1", optional = true } json5_rs = { version = "0.4.1", optional = true, package = "json5" } corn = { version = "0.10.0", optional = true, package = "libcorn" } +dotenvy = { version = "0.15.7", optional = true } indexmap = { version = "2.11.4", features = ["serde"], optional = true } convert_case = { version = "0.6.0", optional = true } pathdiff = "0.2.3" diff --git a/src/file/format/dotenv.rs b/src/file/format/dotenv.rs new file mode 100644 index 00000000..8e000711 --- /dev/null +++ b/src/file/format/dotenv.rs @@ -0,0 +1,31 @@ +use std::error::Error; +use std::io::Cursor; + +use dotenvy::from_read_iter; + +use crate::map::Map; +use crate::value::{Value, ValueKind}; + +pub(crate) fn parse( + uri: Option<&String>, + text: &str, +) -> Result, Box> { + let mut map: Map = Map::new(); + let cursor = Cursor::new(text); + + for item in from_read_iter(cursor) { + let (key, mut value) = item?; + + let os_env_vars = std::env::vars_os(); + for (os_string_key, os_string_value) in os_env_vars { + let string_key: String = os_string_key.to_string_lossy().into_owned(); + if string_key == key { + value = os_string_value.to_string_lossy().into_owned(); + } + } + + map.insert(key, Value::new(uri, ValueKind::String(value))); + } + + Ok(map) +} diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs index bb3df49d..483fdfb5 100644 --- a/src/file/format/mod.rs +++ b/src/file/format/mod.rs @@ -24,6 +24,9 @@ mod json5; #[cfg(feature = "corn")] mod corn; +#[cfg(feature = "dotenv")] +mod dotenv; + /// File formats provided by the library. /// /// 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 { /// Corn (parsed with `libcorn`) #[cfg(feature = "corn")] Corn, + + /// Dotenv (parsed with `dotenvy`) + #[cfg(feature = "dotenv")] + Dotenv, } impl FileFormat { @@ -76,6 +83,8 @@ impl FileFormat { FileFormat::Json5, #[cfg(feature = "corn")] FileFormat::Corn, + #[cfg(feature = "dotenv")] + FileFormat::Dotenv, ] } @@ -102,6 +111,9 @@ impl FileFormat { #[cfg(feature = "corn")] FileFormat::Corn => &["corn"], + #[cfg(feature = "dotenv")] + FileFormat::Dotenv => &["dotenv"], + #[cfg(all( not(feature = "toml"), not(feature = "json"), @@ -109,6 +121,7 @@ impl FileFormat { not(feature = "ini"), not(feature = "ron"), not(feature = "json5"), + not(feature = "dotenv"), ))] _ => unreachable!("No features are enabled, this library won't work without features"), } @@ -141,6 +154,9 @@ impl FileFormat { #[cfg(feature = "corn")] FileFormat::Corn => corn::parse(uri, text), + #[cfg(feature = "dotenv")] + FileFormat::Dotenv => dotenv::parse(uri, text), + #[cfg(all( not(feature = "toml"), not(feature = "json"), @@ -148,6 +164,7 @@ impl FileFormat { not(feature = "ini"), not(feature = "ron"), not(feature = "json5"), + not(feature = "dotenv"), ))] _ => unreachable!("No features are enabled, this library won't work without features"), } diff --git a/tests/testsuite/file_dotenv.rs b/tests/testsuite/file_dotenv.rs new file mode 100644 index 00000000..1f9066a7 --- /dev/null +++ b/tests/testsuite/file_dotenv.rs @@ -0,0 +1,78 @@ +#![cfg(feature = "dotenv")] + +use config::Config; +use std::env; + +#[test] +fn basic_dotenv() { + let s = Config::builder() + .add_source(config::File::from_str( + r#" + FOO=bar + BAZ=qux + "#, + config::FileFormat::Dotenv, + )) + .build() + .unwrap(); + + assert_eq!(s.get::("FOO").unwrap(), "bar"); + assert_eq!(s.get::("BAZ").unwrap(), "qux"); +} + +#[test] +fn optional_variables() { + let s = Config::builder() + .add_source(config::File::from_str( + r#" + FOO=bar + BAZ=${FOO} + BAR=${UNDEFINED:-} + "#, + config::FileFormat::Dotenv, + )) + .build() + .unwrap(); + + assert_eq!(s.get::("BAR").unwrap(), ""); +} + +#[test] +fn multiple_files() { + let s = Config::builder() + .add_source(config::File::from_str( + r#" + FOO=bar + "#, + config::FileFormat::Dotenv, + )) + .add_source(config::File::from_str( + r#" + BAZ=qux + "#, + config::FileFormat::Dotenv, + )) + .build() + .unwrap(); + + assert_eq!(s.get::("FOO").unwrap(), "bar"); + assert_eq!(s.get::("BAZ").unwrap(), "qux"); +} + +#[test] +fn environment_overrides() { + env::set_var("FOOBAR", "env_value"); + + let s = Config::builder() + .add_source(config::File::from_str( + r#" + FOOBAR=file_value + "#, + config::FileFormat::Dotenv, + )) + .add_source(config::Environment::with_prefix("env").separator("_")) + .build() + .unwrap(); + + assert_eq!(s.get::("FOOBAR").unwrap(), "env_value"); +} diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 6f9759c8..68b795d5 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -9,6 +9,7 @@ pub mod env; pub mod errors; pub mod file; pub mod file_corn; +pub mod file_dotenv; pub mod file_ini; pub mod file_json; pub mod file_json5;