bibtex-parser 0.2.0

BibTeX parser with Rust and Python APIs
Documentation
use bibtex_parser::{DiagnosticCode, DiagnosticSeverity, DiagnosticTarget, ParseStatus, Parser};

fn first_diagnostic(input: &str) -> (DiagnosticCode, DiagnosticTarget) {
    let document = Parser::new().tolerant().parse_document(input).unwrap();
    assert_eq!(document.status(), ParseStatus::Partial);
    (
        document.diagnostics()[0].code.clone(),
        document.diagnostics()[0].target.clone(),
    )
}

#[test]
fn diagnostic_code_inventory_is_stable() {
    let ok = "\n@book{ok, title = \"Recovered\"}";
    let cases = [
        (
            format!("@article{{, title = \"A\"}}{ok}"),
            DiagnosticCode::MISSING_ENTRY_KEY,
            DiagnosticTarget::FailedBlock(0),
        ),
        (
            format!("@article{{bad, title \"A\"}}{ok}"),
            DiagnosticCode::MISSING_FIELD_SEPARATOR,
            DiagnosticTarget::FailedBlock(0),
        ),
        (
            format!("@article{{bad, = \"A\"}}{ok}"),
            DiagnosticCode::EXPECTED_FIELD_NAME,
            DiagnosticTarget::FailedBlock(0),
        ),
        (
            format!("@article{{bad, title = , year = 2020}}{ok}"),
            DiagnosticCode::EMPTY_FIELD_VALUE,
            DiagnosticTarget::FailedBlock(0),
        ),
        (
            format!("@article{{bad, title = # \"A\"}}{ok}"),
            DiagnosticCode::EXPECTED_VALUE_ATOM,
            DiagnosticTarget::FailedBlock(0),
        ),
        (
            format!("@article{{bad, title = \"A\" year = 2020}}{ok}"),
            DiagnosticCode::BAD_FIELD_BOUNDARY,
            DiagnosticTarget::Entry(0),
        ),
        (
            format!("@article{{bad, title = \"A\" # , year = 2020}}{ok}"),
            DiagnosticCode::BAD_VALUE_BOUNDARY,
            DiagnosticTarget::FailedBlock(0),
        ),
        (
            format!("@article{{bad, title = \"A\"\n{ok}"),
            DiagnosticCode::UNCLOSED_ENTRY,
            DiagnosticTarget::Entry(0),
        ),
        (
            format!("@article{{bad, title = {{A\n{ok}"),
            DiagnosticCode::UNCLOSED_BRACED_VALUE,
            DiagnosticTarget::FailedBlock(0),
        ),
        (
            format!("@article{{bad, title = \"A\n{ok}"),
            DiagnosticCode::UNCLOSED_QUOTED_VALUE,
            DiagnosticTarget::FailedBlock(0),
        ),
    ];

    for (input, expected_code, expected_target) in cases {
        assert_eq!(first_diagnostic(&input), (expected_code, expected_target));
    }
}

#[test]
fn strict_document_parse_returns_failed_status_with_structured_diagnostic() {
    let input = "@article{bad, title = \"A\"";

    assert!(Parser::new().parse(input).is_err());
    let document = Parser::new().parse_document(input).unwrap();
    let summary = document.summary();

    assert_eq!(document.status(), ParseStatus::Failed);
    assert_eq!(summary.status, ParseStatus::Failed);
    assert_eq!(summary.entries, 0);
    assert_eq!(summary.errors, 1);
    assert_eq!(summary.failed_blocks, 1);
    assert_eq!(document.blocks().len(), 1);
    assert_eq!(
        document.diagnostics()[0].code,
        DiagnosticCode::UNCLOSED_ENTRY
    );
    assert_eq!(
        document.diagnostics()[0].severity,
        DiagnosticSeverity::Error
    );
    assert!(document.diagnostics()[0].source.is_some());
}

#[test]
fn snippets_cover_beginning_middle_multiline_unicode_and_eof() {
    let beginning = Parser::new()
        .tolerant()
        .parse_document("@article{, title = \"A\"}\n@book{ok, title = \"B\"}")
        .unwrap();
    assert!(beginning.diagnostics()[0]
        .snippet
        .as_deref()
        .unwrap()
        .contains("@article"));

    let middle = Parser::new()
        .tolerant()
        .parse_document(
            "@book{good, title = \"B\"}\n@article{bad, title = }\n@book{ok, title = \"B\"}",
        )
        .unwrap();
    assert!(middle.diagnostics()[0]
        .snippet
        .as_deref()
        .unwrap()
        .contains("@article{bad"));

    let multiline = Parser::new()
        .tolerant()
        .parse_document(
            "@article{bad,\n  title = {Line one\n    Line two\n@book{ok, title = \"B\"}",
        )
        .unwrap();
    assert!(multiline.diagnostics()[0]
        .snippet
        .as_deref()
        .unwrap()
        .contains("Line one"));

    let unicode = Parser::new()
        .tolerant()
        .parse_document("@article{bad, title = \"Café\n@book{ok, title = \"B\"}")
        .unwrap();
    assert!(unicode.diagnostics()[0]
        .snippet
        .as_deref()
        .unwrap()
        .contains("Café"));

    let eof = Parser::new()
        .parse_document("@article{bad, title = \"A\"")
        .unwrap();
    assert!(eof.diagnostics()[0]
        .snippet
        .as_deref()
        .unwrap()
        .contains("@article{bad"));
}