From f479118e867b26bea712ebff62f44a9f56f5b1aa Mon Sep 17 00:00:00 2001 From: Zoey Riordan Date: Mon, 3 Feb 2020 21:28:01 -0800 Subject: [PATCH] recursively load .env files up to root --- dotenv/src/find.rs | 51 ++++++++++++++----------- dotenv/src/iter.rs | 32 ++++++++++++++++ dotenv/src/lib.rs | 47 ++++++++++++++++------- dotenv/tests/common/mod.rs | 23 ++++++++++- dotenv/tests/test-dotenv-iter.rs | 18 +++++++-- dotenv/tests/test-from-filename-iter.rs | 19 +++++++-- dotenv/tests/test-from-filename.rs | 15 +++++++- 7 files changed, 161 insertions(+), 44 deletions(-) diff --git a/dotenv/src/find.rs b/dotenv/src/find.rs index ab44e82..549b8dc 100644 --- a/dotenv/src/find.rs +++ b/dotenv/src/find.rs @@ -21,32 +21,39 @@ impl<'a> Finder<'a> { self } - pub fn find(self) -> Result<(PathBuf, Iter)> { - let path = find(&env::current_dir().map_err(Error::Io)?, self.filename)?; - let file = File::open(&path).map_err(Error::Io)?; - let iter = Iter::new(file); - Ok((path, iter)) + pub fn find(self) -> Result)>> { + let paths = find(&env::current_dir().map_err(Error::Io)?, self.filename)?; + + paths + .into_iter() + .map(|path| match File::open(&path) { + Ok(file) => Ok((path, Iter::new(file))), + Err(err) => Err(Error::Io(err)) + }) + .collect::>>() } } /// Searches for `filename` in `directory` and parent directories until found or root is reached. -pub fn find(directory: &Path, filename: &Path) -> Result { - let candidate = directory.join(filename); - - match fs::metadata(&candidate) { - Ok(metadata) => if metadata.is_file() { - return Ok(candidate); - }, - Err(error) => { - if error.kind() != io::ErrorKind::NotFound { - return Err(Error::Io(error)); - } - } +pub fn find(directory: &Path, filename: &Path) -> Result> { + + let results = directory + .ancestors() + .map(|path| path.join(filename)) + .filter_map(|candidate| match fs::metadata(&candidate) { + Ok(metadata) if metadata.is_file() => { + Some(Ok(candidate)) + }, + Err(error) if error.kind() != io::ErrorKind::NotFound => { + Some(Err(Error::Io(error))) + }, + _ => None, + }) + .collect::>>()?; + + if results.is_empty() { + return Err(Error::Io(io::Error::new(io::ErrorKind::NotFound, "path not found"))); } - if let Some(parent) = directory.parent() { - find(parent, filename) - } else { - Err(Error::Io(io::Error::new(io::ErrorKind::NotFound, "path not found"))) - } + Ok(results) } diff --git a/dotenv/src/iter.rs b/dotenv/src/iter.rs index f38b447..99d1a62 100644 --- a/dotenv/src/iter.rs +++ b/dotenv/src/iter.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::collections::HashMap; use std::env; use std::io::{BufReader, Lines}; @@ -50,3 +51,34 @@ impl Iterator for Iter { } } } + +pub struct DistinctEnvIter { + seen_keys: HashSet, + inner: T, +} + +impl>> DistinctEnvIter { + pub fn new(inner_iter: T) -> DistinctEnvIter { + DistinctEnvIter { + seen_keys: HashSet::new(), + inner: inner_iter + } + } +} + +impl>> Iterator for DistinctEnvIter { + type Item = Result<(String, String)>; + + fn next(&mut self) -> Option { + loop { + match self.inner.next() { + Some(Ok((key, value))) if !self.seen_keys.contains(&key) => { + self.seen_keys.insert(key.clone()); + return Some(Ok((key, value))); + }, + Some(Ok(_)) => continue, + otherwise => return otherwise, + } + } + } +} diff --git a/dotenv/src/lib.rs b/dotenv/src/lib.rs index 1368a48..34a219e 100644 --- a/dotenv/src/lib.rs +++ b/dotenv/src/lib.rs @@ -17,7 +17,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Once}; pub use crate::errors::*; -use crate::iter::Iter; +use crate::iter::{Iter, DistinctEnvIter}; use crate::find::Finder; static START: Once = Once::new(); @@ -120,10 +120,15 @@ pub fn from_path_iter>(path: P) -> Result> { /// use dotenv; /// dotenv::from_filename(".env").ok(); /// ``` -pub fn from_filename>(filename: P) -> Result { - let (path, iter) = Finder::new().filename(filename.as_ref()).find()?; - iter.load()?; - Ok(path) +pub fn from_filename>(filename: P) -> Result> { + let results = Finder::new().filename(filename.as_ref()).find()?; + results + .into_iter() + .map(|(path, iter)| -> Result { + iter.load()?; + Ok(path) + }) + .collect() } /// Like `from_filename`, but returns an iterator over variables instead of loading into environment. @@ -147,9 +152,13 @@ pub fn from_filename>(filename: P) -> Result { /// } /// ``` #[deprecated(since = "0.14.1", note = "please use `from_path` in conjunction with `var` instead")] -pub fn from_filename_iter>(filename: P) -> Result> { - let (_, iter) = Finder::new().filename(filename.as_ref()).find()?; - Ok(iter) +pub fn from_filename_iter>(filename: P) -> Result>> { + let results = Finder::new().filename(filename.as_ref()).find()?; + Ok(DistinctEnvIter::new(results + .into_iter() + .flat_map(|(_, iter)| { + iter + }))) } /// This is usually what you want. @@ -161,9 +170,14 @@ pub fn from_filename_iter>(filename: P) -> Result> { /// dotenv::dotenv().ok(); /// ``` pub fn dotenv() -> Result { - let (path, iter) = Finder::new().find()?; - iter.load()?; - Ok(path) + let results = Finder::new().find()?; + results + .into_iter() + .map(|(path, iter)| -> Result { + iter.load()?; + Ok(path) + }) + .collect() } /// Like `dotenv`, but returns an iterator over variables instead of loading into environment. @@ -178,7 +192,12 @@ pub fn dotenv() -> Result { /// } /// ``` #[deprecated(since = "0.14.1", note = "please use `from_path` in conjunction with `var` instead")] -pub fn dotenv_iter() -> Result> { - let (_, iter) = Finder::new().find()?; - Ok(iter) +pub fn dotenv_iter() -> Result>> { + let results = Finder::new().find()?; + + Ok(DistinctEnvIter::new(results + .into_iter() + .flat_map(|(_, iter)| { + iter + }))) } diff --git a/dotenv/tests/common/mod.rs b/dotenv/tests/common/mod.rs index 6bdbb8e..25378fd 100644 --- a/dotenv/tests/common/mod.rs +++ b/dotenv/tests/common/mod.rs @@ -14,6 +14,27 @@ pub fn tempdir_with_dotenv(dotenv_text: &str) -> io::Result { } pub fn make_test_dotenv() -> io::Result { - tempdir_with_dotenv("TESTKEY=test_val") + tempdir_with_dotenv("TESTKEY=test_val") +} + +pub fn make_layered_test_dotenv() -> io::Result { + let dir = tempdir()?; + env::set_current_dir(dir.path())?; + + let dotenv_path = dir.path().join(".env"); + let mut dotenv_file = File::create(dotenv_path)?; + dotenv_file.write_all("TESTKEY=test_val\nTESTKEY2=test_val_outer".as_bytes())?; + dotenv_file.sync_all()?; + + let inner_dir = dir.path().join("inner"); + std::fs::create_dir(&inner_dir)?; + env::set_current_dir(&inner_dir)?; + + let inner_dotenv_path = inner_dir.join(".env"); + let mut inner_dotenv_file = File::create(inner_dotenv_path)?; + inner_dotenv_file.write_all("TESTKEY2=test_val_inner".as_bytes())?; + inner_dotenv_file.sync_all()?; + + Ok(dir) } diff --git a/dotenv/tests/test-dotenv-iter.rs b/dotenv/tests/test-dotenv-iter.rs index 3fcc46b..2cd37b8 100644 --- a/dotenv/tests/test-dotenv-iter.rs +++ b/dotenv/tests/test-dotenv-iter.rs @@ -12,11 +12,23 @@ fn test_dotenv_iter() { let iter = dotenv_iter().unwrap(); - assert!(env::var("TESTKEY").is_err()); + iter.filter_map(Result::ok).any(|(key, value)| key == "TESTKEY" && value == "test_val"); - iter.load().ok(); + env::set_current_dir(dir.path().parent().unwrap()).unwrap(); + dir.close().unwrap(); +} + +#[test] +#[allow(deprecated)] +fn test_dotenv_subdir_iter() { + let dir = make_layered_test_dotenv().unwrap(); + + let iter = dotenv_iter().unwrap(); - assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); + let pairs = iter.filter_map(Result::ok).collect::>(); + + assert!(pairs.contains(&("TESTKEY".into(), "test_val".into()))); + assert!(pairs.contains(&("TESTKEY2".into(), "test_val_inner".into()))); env::set_current_dir(dir.path().parent().unwrap()).unwrap(); dir.close().unwrap(); diff --git a/dotenv/tests/test-from-filename-iter.rs b/dotenv/tests/test-from-filename-iter.rs index cb1867b..0aa76cc 100644 --- a/dotenv/tests/test-from-filename-iter.rs +++ b/dotenv/tests/test-from-filename-iter.rs @@ -12,12 +12,25 @@ fn test_from_filename_iter() { let iter = from_filename_iter(".env").unwrap(); - assert!(env::var("TESTKEY").is_err()); + iter.filter_map(Result::ok).any(|(key, value)| key == "TESTKEY" && value == "test_val"); - iter.load().ok(); + env::set_current_dir(dir.path().parent().unwrap()).unwrap(); + dir.close().unwrap(); +} - assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); +#[test] +#[allow(deprecated)] +fn test_from_filename_subdir_iter() { + let dir = make_layered_test_dotenv().unwrap(); + + let iter = from_filename_iter(".env").unwrap(); + + let pairs = iter.filter_map(Result::ok).collect::>(); + + assert!(pairs.contains(&("TESTKEY".into(), "test_val".into()))); + assert!(pairs.contains(&("TESTKEY2".into(), "test_val_inner".into()))); env::set_current_dir(dir.path().parent().unwrap()).unwrap(); dir.close().unwrap(); } + diff --git a/dotenv/tests/test-from-filename.rs b/dotenv/tests/test-from-filename.rs index bdcf49e..0301172 100644 --- a/dotenv/tests/test-from-filename.rs +++ b/dotenv/tests/test-from-filename.rs @@ -9,10 +9,23 @@ use crate::common::*; fn test_from_filename() { let dir = make_test_dotenv().unwrap(); - from_filename(".env").ok(); + from_filename(".env").unwrap(); assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); env::set_current_dir(dir.path().parent().unwrap()).unwrap(); dir.close().unwrap(); } + +#[test] +fn test_from_filename_subdir() { + let dir = make_layered_test_dotenv().unwrap(); + + from_filename(".env").unwrap(); + + assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); + assert_eq!(env::var("TESTKEY2").unwrap(), "test_val_inner"); + + env::set_current_dir(dir.path().parent().unwrap()).unwrap(); + dir.close().unwrap(); +}