Skip to content

Commit

Permalink
Expose parse errors via Dotenv::iter.
Browse files Browse the repository at this point in the history
Refactor `dotenv::dotenv::Iter` to expose parse errors for those
interested. The parse errors are structured as opaque strings to hide
the parsing implementation details (currently nom and nom's errors are
the internal currency).

This allows a consumer that cares about propagating errors in `.env`
files to use something like:
```
let env = dotenv::from_filename(".env")?;
let mut iter = env.iter();
while let Some((key, value)) = iter.try_next()? {
    if std::env::var(key).is_err() {
        std::env::set_var(key, value);
    }
}
```

Fixes #4
  • Loading branch information
jsirois committed Nov 4, 2023
1 parent 60dd8df commit a2d2026
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 26 deletions.
36 changes: 20 additions & 16 deletions src/dotenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,32 @@ impl<'a> Iter<'a> {
Value::List(list) => Some(list.into_iter().flat_map(|it| self.resolve(it)).collect()),
}
}

pub fn try_next(&mut self) -> crate::Result<Option<(&'a str, String)>> {
while !self.input.is_empty() {
match parse(self.input) {
Ok((rest, maybe)) => {
self.input = rest; // set next input

if let Some((key, value)) = maybe {
if let Some(value) = self.resolve(value) {
self.resolved.insert(key, value.clone());
return Ok(Some((key, value)));
}
}
}
Err(err) => return Err(crate::Error::Parse(format!("{err}"))),
}
}
Ok(None)
}
}

impl<'a> Iterator for Iter<'a> {
type Item = (&'a str, String);

fn next(&mut self) -> Option<Self::Item> {
while let Ok((rest, maybe)) = parse(self.input) {
self.input = rest; // set next input

if let Some((key, value)) = maybe {
if let Some(value) = self.resolve(value) {
self.resolved.insert(key, value.clone());
return Some((key, value));
}
}

if rest.is_empty() {
break;
}
}

None
self.try_next().unwrap_or_default()
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use std::env;
use std::error;
use std::fmt;
use std::fmt::Display;
use std::io;

#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Io(io::Error),
Env(env::VarError),
Parse(String),
}

impl Error {
Expand All @@ -29,6 +31,7 @@ impl fmt::Display for Error {
match self {
Error::Io(err) => err.fmt(fmt),
Error::Env(err) => err.fmt(fmt),
Error::Parse(err) => err.fmt(fmt),
}
}
}
Expand All @@ -38,6 +41,7 @@ impl error::Error for Error {
match self {
Error::Io(err) => Some(err),
Error::Env(err) => Some(err),
Error::Parse(_) => None,
}
}
}
Expand All @@ -53,3 +57,9 @@ impl From<env::VarError> for Error {
Error::Env(err)
}
}

impl<I: Display> From<nom::error::Error<I>> for Error {
fn from(err: nom::error::Error<I>) -> Self {
Error::Parse(format!("{err}"))
}
}
1 change: 0 additions & 1 deletion tests/fixtures/sample-basic.env
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS=`double "quotes" and single 'quotes' w
EXPAND_NEWLINES="expand\nnew\nlines"
DONT_EXPAND_UNQUOTED=dontexpand\nnewlines
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
# COMMENTS=work
INLINE_COMMENTS=inline comments # work #very #well
INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work
Expand Down
19 changes: 19 additions & 0 deletions tests/test-dotenv-try-next.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
mod fixtures;
use fixtures::*;

#[test]
fn test_propagate_env_parse_errors() -> anyhow::Result<()> {
let (_t, mut exps) = with_basic_dotenv()?;

// This is an example of how a consumer that cares about invalid `.env` files can handle them using the
// `Iter::try_next` API (see: https://github.com/arniu/dotenvs-rs/issues/4)
let env = dotenv::from_filename(".env")?;
let mut iter = env.iter();
while let Some((key, value)) = iter.try_next()? {
let expected = exps.remove(key).unwrap();
assert_eq!(expected, value, "check {}", key);
}
assert!(exps.is_empty());

Ok(())
}
44 changes: 44 additions & 0 deletions tests/test-sample-bad.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use dotenv::Error;
use std::collections::HashMap;
use std::iter::{IntoIterator, Iterator};

const BAD_ENV: &str = r#"
A=foo bar
B="notenough
C='toomany''
D=valid
export NOT_SET
E=valid
"#;

#[test]
fn test_bad_env() -> anyhow::Result<()> {
let env = dotenv::from_read(BAD_ENV.as_bytes())?;

assert_eq!(
vec![
("A", "foo bar".into()),
("B", "\"notenough".into()),
("C", "toomany".into())
]
.into_iter()
.collect::<HashMap<_, _>>(),
env.iter().collect::<HashMap<_, _>>()
);

let mut iter = env.iter();
assert_eq!(Some(("A", "foo bar".into())), iter.try_next()?);
assert_eq!(Some(("B", "\"notenough".into())), iter.try_next()?);
assert_eq!(Some(("C", "toomany".into())), iter.try_next()?);

// TODO: Use assert_matches! when it stabilizes: https://github.com/rust-lang/rust/issues/82775
match iter.try_next().unwrap_err() {
Error::Parse(err) => assert_eq!(
"Parsing Error: Error { input: \"'\\nD=valid\\nexport NOT_SET\\nE=valid\\n\", code: Tag }",
err
),
err => panic!("Unexpected error variant: {err:?}", err = err),
}

Ok(())
}
7 changes: 4 additions & 3 deletions tests/test-sample-basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ use fixtures::*;

#[test]
fn test_sample() -> anyhow::Result<()> {
let (_t, exps) = with_basic_dotenv()?;
let (_t, mut exps) = with_basic_dotenv()?;
for (key, value) in dotenv::from_filename(".env")?.iter() {
let expected = exps.get(key).unwrap();
assert_eq!(expected, &value, "check {}", key);
let expected = exps.remove(key).unwrap();
assert_eq!(expected, value, "check {}", key);
}
assert!(exps.is_empty());

Ok(())
}
7 changes: 4 additions & 3 deletions tests/test-sample-expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ use fixtures::*;

#[test]
fn test_sample() -> anyhow::Result<()> {
let (_t, exps) = with_expand_dotenv()?;
let (_t, mut exps) = with_expand_dotenv()?;
for (key, value) in dotenv::from_filename(".env")?.iter() {
let expected = exps.get(key).unwrap();
assert_eq!(expected, &value, "check {}", key);
let expected = exps.remove(key).unwrap();
assert_eq!(expected, value, "check {}", key);
}
assert!(exps.is_empty());

Ok(())
}
7 changes: 4 additions & 3 deletions tests/test-sample-multiline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ use fixtures::*;

#[test]
fn test_sample() -> anyhow::Result<()> {
let (_t, exps) = with_multiline_dotenv()?;
let (_t, mut exps) = with_multiline_dotenv()?;
for (key, value) in dotenv::from_filename(".env")?.iter() {
let expected = exps.get(key).unwrap();
assert_eq!(expected, &value, "check {}", key);
let expected = exps.remove(key).unwrap();
assert_eq!(expected, value, "check {}", key);
}
assert!(exps.is_empty());

Ok(())
}

0 comments on commit a2d2026

Please sign in to comment.