openbim-dt 0.2.0

Lossless ISO 23387 data-template model and XML codec for Rust.
Documentation
use std::str::FromStr;

use openbim_dt::{
    AnyUri, Base, Concept, DataTypeName, DateTime, Decimal, Guid, Language, MultiLanguageText,
    PositiveInteger, Rational, Reference, Scale,
};

#[test]
fn guid_preserves_lexical_form_after_validation() {
    let upper = "ABCDEF12-3456-7890-ABCD-EF1234567890";
    let guid = Guid::from_str(upper).expect("valid GUID");
    assert_eq!(guid.as_str(), upper);
    assert_eq!(guid.to_string(), upper);

    for invalid in [
        "",
        "abcdef12-3456-7890-abcd-ef123456789",
        "abcdef123456-7890-abcd-ef1234567890",
        "zbcdef12-3456-7890-abcd-ef1234567890",
    ] {
        assert!(Guid::from_str(invalid).is_err(), "accepted {invalid:?}");
    }
}

#[test]
fn standard_value_types_are_forward_compatible() {
    assert_eq!(DataTypeName::from("STRING"), DataTypeName::String);
    assert_eq!(
        DataTypeName::from("FUTURE"),
        DataTypeName::Other("FUTURE".into())
    );
    assert_eq!(DataTypeName::String.as_str(), "STRING");

    assert_eq!(Scale::from("LINEAR"), Scale::Linear);
    assert_eq!(Scale::from("CURVED"), Scale::Other("CURVED".into()));
    assert_eq!(Base::from("PI"), Base::Pi);
    assert_eq!(Base::from("TAU"), Base::Other("TAU".into()));
}

#[test]
fn rational_validation_keeps_source_lexeme() {
    for valid in ["0", "-3", "+12", "2/3", "-10/7"] {
        let value = Rational::from_str(valid).expect("valid rational");
        assert_eq!(value.as_str(), valid);
    }
    for invalid in ["", "1/0", "1/02", "1.0", "--1", "1/"] {
        assert!(Rational::from_str(invalid).is_err(), "accepted {invalid:?}");
    }
}

#[test]
fn decimal_and_language_contracts_match_xml_schema_lexical_spaces() {
    for (source, normalized) in [
        ("0", "0"),
        ("-3", "-3"),
        ("+12.50", "+12.50"),
        (".5", ".5"),
        ("5.", "5."),
        (" \t 1.0\n", "1.0"),
    ] {
        assert_eq!(Decimal::from_str(source).unwrap().as_str(), normalized);
    }
    for invalid in ["", ".", "1e3", "--1", "1.2.3"] {
        assert!(Decimal::from_str(invalid).is_err(), "accepted {invalid:?}");
    }
    assert_eq!(Language::from_str(" en-US\n").unwrap().as_str(), "en-US");
    assert!(Language::from_str("de CH").is_err());

    assert_eq!(
        PositiveInteger::from_str(" +004 ").unwrap().as_str(),
        "+004"
    );
    assert!(PositiveInteger::from_str("0").is_err());
    assert!(PositiveInteger::from_str("-1").is_err());
}

#[test]
fn multilingual_text_and_references_are_dt_owned_contracts() {
    let text = MultiLanguageText::new("de", "Feuerwiderstand").unwrap();
    assert_eq!(text.language(), "de");
    assert_eq!(text.text(), "Feuerwiderstand");

    let guid = Guid::from_str("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa").unwrap();
    let reference = Reference::new(
        Some(guid.clone()),
        Some(AnyUri::from_str(" urn:test:property ").unwrap()),
    );
    assert_eq!(reference.guid(), Some(&guid));
    assert_eq!(reference.uri(), Some("urn:test:property"));
    assert!(Reference::new(None, None).is_empty());
    assert!(Reference::identified(None, None).is_err());
    assert_eq!(
        AnyUri::from_str(" https://example.com/café path ")
            .unwrap()
            .as_str(),
        "https://example.com/café path"
    );
    assert_eq!(AnyUri::from_str("a[b]").unwrap().as_str(), "a[b]");
    assert!(AnyUri::from_str("a\0b").is_err());
}

#[test]
fn concept_contract_is_reusable_by_standards_that_extend_concept_type() {
    let guid = Guid::from_str("44444444-4444-4444-4444-444444444444").unwrap();
    let mut concept = Concept::new(
        guid.clone(),
        DateTime::from_str(" 2026-08-25T00:00:00Z ").unwrap(),
        MultiLanguageText::new("en", "Fire safety purpose").unwrap(),
        MultiLanguageText::new("en", "Synthetic definition").unwrap(),
    );
    concept.add_reference(Reference::new(Some(guid), None));

    assert_eq!(
        concept.guid().as_str(),
        "44444444-4444-4444-4444-444444444444"
    );
    assert_eq!(concept.names()[0].text(), "Fire safety purpose");
    assert_eq!(concept.definition().text(), "Synthetic definition");
    assert_eq!(concept.references().len(), 1);
    assert_eq!(concept.date_of_creation(), "2026-08-25T00:00:00Z");
    assert!(DateTime::from_str("not-a-date").is_err());
    assert!(DateTime::from_str("02024-01-01T00:00:00Z").is_err());
    assert!(DateTime::from_str("2024-01-01T24:00:00.0Z").is_ok());
    assert!(DateTime::from_str("2024-01-01T24:00:00.001Z").is_err());
}