adotenv 0.6.0

A tooling-grade .env parser: literal values (no variable expansion), exact byte spans, and edits that leave the rest of the file untouched.
Documentation
use alloc::{string::String, vec::Vec};

use proptest::prelude::*;

use crate::{parse, parse_strict, parse_with, AssignmentState, Dialect, InlineComments, ItemKind};

#[test]
fn values_are_blanked_and_comments_and_layout_are_kept() {
    let source = "# app\n\nA=1   \nexport B=\"two words\" # note\nBARE\nC=''\n";
    let doc = parse_strict(source).unwrap();
    assert_eq!(doc.example(), "# app\n\nA=\nexport B= # note\nBARE\nC=\n");
    let example_text = doc.example();
    let example = parse_strict(&example_text).unwrap();
    assert_eq!(example.get("A"), Some(""));
    assert_eq!(example.get("B"), Some(""));
    assert_eq!(example.get("BARE"), None);
    assert_eq!(example.get("C"), Some(""));
    assert_eq!(
        example.unique("B").unwrap().inline_comment(),
        Some("# note")
    );
}

#[test]
fn multiline_values_are_removed_without_leaving_body_lines() {
    let source = "A=\"first\nsecond\"\nB=2\n";
    let doc = parse_strict(source).unwrap();
    assert_eq!(doc.example(), "A=\"\"\nB=\n");
    let example_text = doc.example();
    let example = parse_strict(&example_text).unwrap();
    assert_eq!(example.get("A"), Some(""));
    assert_eq!(example.get("B"), Some(""));
}

#[test]
fn malformed_and_ignored_statements_cannot_leak_values() {
    let source = "BAD KEY=secret\n  ALSO BAD=other\n";
    let (doc, _) = parse(source);
    assert_eq!(doc.example(), "#\n  #\n");
    let example_text = doc.example();
    let (example, _) = parse(&example_text);
    assert_eq!(example.entries().count(), 0);
}

#[test]
fn compose_inline_comments_keep_an_empty_quoted_placeholder() {
    let source = "B= value   # note\n";
    let (doc, _) = parse_with(source, Dialect::COMPOSE);
    let example = doc.example();
    assert_eq!(example, "B= \"\"   # note\n");
    let (after, _) = parse_with(&example, Dialect::COMPOSE);
    assert_eq!(after.unique("B").unwrap().value(), Some(""));
    assert!(after
        .items()
        .filter(|item| item.kind() == ItemKind::Comment)
        .any(|item| item.raw().contains("# note")));
}

#[test]
fn compose_quoted_trailing_comments_are_retained_as_comments() {
    let source = "B=\"value\"   # note\n";
    let (doc, _) = parse_with(source, Dialect::COMPOSE);
    let example = doc.example();
    assert_eq!(example, "B=\"\"\n   # note\n");
    let (after, _) = parse_with(&example, Dialect::COMPOSE);
    assert_eq!(after.unique("B").unwrap().value(), Some(""));
    assert!(after
        .items()
        .filter(|item| item.kind() == ItemKind::Comment)
        .any(|item| item.raw().contains("# note")));
}

#[test]
fn empty_key_entries_are_commented_without_retaining_their_value() {
    let source = "= value\n";
    let (doc, _) = parse_with(source, Dialect::COMPOSE);
    let example = doc.example();
    assert_eq!(example, "#\n");
    let (after, _) = parse_with(&example, Dialect::COMPOSE);
    assert_eq!(after.entries().count(), 0);
}

#[test]
fn python_adjacent_quoted_comments_keep_an_empty_placeholder() {
    let source = "A=\"value\"#note\n";
    let (doc, _) = parse_with(source, Dialect::PYTHON_DOTENV);
    let example = doc.example();
    assert_eq!(example, "A=\"\"#note\n");
    let (after, _) = parse_with(&example, Dialect::PYTHON_DOTENV);
    assert_eq!(after.unique("A").unwrap().value(), Some(""));
    assert_eq!(after.unique("A").unwrap().inline_comment(), Some("#note"));
}

#[test]
fn node_colon_values_keep_an_empty_placeholder() {
    let source = "A: value\n";
    let (doc, _) = parse_with(source, Dialect::NODE_DOTENV);
    let example = doc.example();
    assert_eq!(example, "A: \"\"\n");
    let (after, _) = parse_with(&example, Dialect::NODE_DOTENV);
    assert_eq!(after.unique("A").unwrap().value(), Some(""));
}

#[test]
fn custom_space_comments_move_to_a_safe_comment_line() {
    let dialect = Dialect {
        inline_comments: InlineComments::Space,
        quoted_comments: false,
        ..Dialect::PORTABLE
    };
    let source = "A=value # note\n";
    let (doc, _) = parse_with(source, dialect);
    let example = doc.example();
    assert_eq!(example, "A=\n# note\n");
    let (after, _) = parse_with(&example, dialect);
    assert_eq!(after.unique("A").unwrap().value(), Some(""));
    assert!(after
        .items()
        .any(|item| item.kind() == ItemKind::Comment && item.raw().contains("# note")));
}

#[test]
fn raw_values_and_bom_are_handled() {
    let source = "\u{feff}A= raw value  \n";
    let (doc, _) = parse_with(source, Dialect::DOCKER_RAW);
    assert_eq!(doc.example(), "\u{feff}A=\n");
}

#[test]
fn example_preserves_keys_and_replaces_all_assigned_states() {
    let source = "A=1\nB=\nC\n";
    let doc = parse_strict(source).unwrap();
    let example_text = doc.example();
    let example = parse_strict(&example_text).unwrap();
    let original: Vec<_> = doc.entries().map(crate::Entry::key).collect();
    let generated: Vec<_> = example.entries().map(crate::Entry::key).collect();
    assert_eq!(original, generated);
    assert_eq!(
        example
            .entries()
            .map(|entry| (entry.key(), entry.state()))
            .collect::<Vec<_>>(),
        [
            ("A", AssignmentState::Empty),
            ("B", AssignmentState::Empty),
            ("C", AssignmentState::Bare),
        ]
    );
}

#[test]
fn comments_are_kept_even_when_they_contain_value_like_text() {
    let source = "# A=secret\nA=value\n";
    let doc = parse_strict(source).unwrap();
    assert_eq!(doc.example(), "# A=secret\nA=\n");
}

proptest! {
    #[test]
    fn example_preserves_entry_keys_and_never_retains_assigned_values(
        source in "\\PC{0,240}",
    ) {
        let (doc, _) = parse(&source);
        let example_text: String = doc.example();
        let (example, _) = parse(&example_text);
        let expected: Vec<_> = doc
            .entries()
            .filter(|entry| !entry.key().is_empty())
            .collect();
        prop_assert_eq!(expected.len(), example.entries().len());
        for (left, right) in expected.into_iter().zip(example.entries()) {
            prop_assert_eq!(left.key(), right.key());
            if left.state() == AssignmentState::Bare {
                prop_assert_eq!(right.state(), AssignmentState::Bare);
                prop_assert_eq!(right.value(), None);
            } else {
                prop_assert_eq!(right.state(), AssignmentState::Empty);
                prop_assert_eq!(right.value(), Some(""));
            }
            prop_assert_eq!(left.inline_comment(), right.inline_comment());
        }
        for item in doc.items() {
            if item.kind() == ItemKind::Comment {
                prop_assert!(example_text.contains(item.raw()));
            }
        }
    }
}