xdoc-rs 0.1.1

Declarative XML engine for Rust
Documentation
mod support;

use proptest::prelude::*;
use proptest::test_runner::TestCaseError;
use xdoc::core::Document;
use xdoc::core::XmlResult;
use xdoc::testing;
use xdoc::writer::{escape_attribute, escape_text, to_string_compact};

const VALID_FIXTURES: &[&str] = &["simple.xml", "namespaces.xml", "mixed_content.xml"];
const INVALID_FIXTURES: &[&str] = &[
    "invalid.xml",
    "mismatched_tags.xml",
    "multiple_roots.xml",
    "unbound_namespace.xml",
];

#[test]
fn testing_valid_fixtures_parse_and_roundtrip() -> XmlResult<()> {
    for fixture in VALID_FIXTURES {
        let result = support::assert_fixture_roundtrip(fixture)?;

        assert_eq!(
            result.compact_xml(),
            result.reserialized_xml(),
            "{fixture} should serialize deterministically"
        );
    }

    Ok(())
}

#[test]
fn testing_invalid_fixtures_are_rejected() -> XmlResult<()> {
    for fixture in INVALID_FIXTURES {
        let xml = support::read_xml_fixture(fixture)?;
        let error = support::parse_xml(&xml).expect_err("invalid XML fixture must fail");

        assert!(
            !error.message().is_empty(),
            "{fixture} should fail with a useful parser error"
        );
    }

    Ok(())
}

#[test]
fn golden_helper_compares_writer_output() -> XmlResult<()> {
    let document = support::parse_xml("<Root><Child>value</Child></Root>")?;
    let xml = to_string_compact(&document)?;

    support::assert_golden("writer_simple.xml", &xml)
}

#[test]
fn golden_helper_reports_actionable_diff() {
    let diff = testing::compare_xml("<Root><A/></Root>", "<Root><B/></Root>")
        .expect_err("XML must differ");

    assert!(diff.summary().contains("line 1"));
    assert!(diff.summary().contains("<Root><A/></Root>"));
    assert!(diff.summary().contains("<Root><B/></Root>"));
}

#[test]
fn roundtrip_helper_rejects_invalid_xml() -> XmlResult<()> {
    let xml = support::read_xml_fixture("mismatched_tags.xml")?;

    let error = testing::assert_compact_roundtrip(&xml).expect_err("roundtrip must reject invalid");

    assert!(!error.message().is_empty());
    Ok(())
}

fn xml_text_strategy() -> impl Strategy<Value = String> {
    prop::collection::vec(xml_text_char_strategy(), 0..64)
        .prop_map(|chars| chars.into_iter().collect())
}

fn xml_text_char_strategy() -> impl Strategy<Value = char> {
    prop_oneof![
        Just('&'),
        Just('<'),
        Just('>'),
        Just('"'),
        Just('\''),
        Just(' '),
        proptest::char::range('a', 'z'),
        proptest::char::range('A', 'Z'),
        proptest::char::range('0', '9'),
    ]
}

fn parse_prop(xml: &str) -> Result<Document, TestCaseError> {
    support::parse_xml(xml).map_err(|error| TestCaseError::fail(error.to_string()))
}

fn serialize_prop(document: &Document) -> Result<String, TestCaseError> {
    to_string_compact(document).map_err(|error| TestCaseError::fail(error.to_string()))
}

proptest! {
    #[test]
    fn property_testing_escape_text_roundtrips(value in xml_text_strategy()) {
        let escaped = escape_text(&value);
        let xml = format!("<Root>{escaped}</Root>");
        let parsed = parse_prop(&xml)?;
        let serialized = serialize_prop(&parsed)?;
        let expected = if value.is_empty() {
            "<Root/>".to_owned()
        } else {
            xml
        };

        prop_assert_eq!(serialized, expected);
    }

    #[test]
    fn property_testing_escape_attribute_roundtrips(value in xml_text_strategy()) {
        let escaped = escape_attribute(&value);
        let xml = format!("<Root value=\"{escaped}\"/>");
        let parsed = parse_prop(&xml)?;
        let serialized = serialize_prop(&parsed)?;

        prop_assert_eq!(serialized, xml);
    }
}