use xmlschema::{parse_schema, validate};
fn restricted(base: &str, facets: &str) -> xmlschema::Schema {
let xsd = format!(
r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="v">
<xs:simpleType>
<xs:restriction base="{base}">{facets}</xs:restriction>
</xs:simpleType>
</xs:element>
</xs:schema>"#
);
parse_schema(&xsd).expect("valid schema")
}
fn report(schema: &xmlschema::Schema, value: &str) -> xmlschema::Report {
let doc = oxml::parse(&format!("<v>{value}</v>")).expect("well-formed");
validate(&doc, schema)
}
fn accepts(schema: &xmlschema::Schema, value: &str) -> bool {
report(schema, value).violations.is_empty()
}
#[test]
fn enumeration_admits_only_listed_values() {
let s = restricted(
"xs:string",
r#"<xs:enumeration value="red"/><xs:enumeration value="green"/>"#,
);
assert!(accepts(&s, "red"));
assert!(accepts(&s, "green"));
assert!(!accepts(&s, "blue"));
assert!(!accepts(&s, ""));
assert!(!accepts(&s, "RED"), "enumeration is case sensitive");
}
#[test]
fn an_enumeration_violation_lists_the_permitted_values() {
let s = restricted(
"xs:string",
r#"<xs:enumeration value="red"/><xs:enumeration value="green"/>"#,
);
let text = report(&s, "blue").to_string();
assert!(text.contains("red"), "{text}");
assert!(text.contains("green"), "{text}");
}
#[test]
fn length_requires_exactly_that_many_characters() {
let s = restricted("xs:string", r#"<xs:length value="3"/>"#);
assert!(accepts(&s, "abc"));
assert!(!accepts(&s, "ab"));
assert!(!accepts(&s, "abcd"));
let text = report(&s, "ab").to_string();
assert!(text.contains('3'), "{text}");
}
#[test]
fn min_and_max_length_bound_each_end() {
let s = restricted(
"xs:string",
r#"<xs:minLength value="2"/><xs:maxLength value="4"/>"#,
);
assert!(!accepts(&s, "a"));
assert!(accepts(&s, "ab"));
assert!(accepts(&s, "abcd"));
assert!(!accepts(&s, "abcde"));
assert!(report(&s, "a").to_string().contains('2'));
assert!(report(&s, "abcde").to_string().contains('4'));
}
#[test]
fn length_counts_characters_not_bytes() {
let s = restricted("xs:string", r#"<xs:length value="3"/>"#);
assert!(
accepts(&s, "éä¸x"),
"three characters, more than three bytes"
);
assert!(!accepts(&s, "éä¸"));
}
#[test]
fn inclusive_bounds_include_their_endpoints() {
let s = restricted(
"xs:integer",
r#"<xs:minInclusive value="1"/><xs:maxInclusive value="10"/>"#,
);
assert!(!accepts(&s, "0"));
assert!(accepts(&s, "1"), "minInclusive must admit its endpoint");
assert!(accepts(&s, "10"), "maxInclusive must admit its endpoint");
assert!(!accepts(&s, "11"));
}
#[test]
fn exclusive_bounds_exclude_their_endpoints() {
let s = restricted(
"xs:integer",
r#"<xs:minExclusive value="1"/><xs:maxExclusive value="10"/>"#,
);
assert!(!accepts(&s, "1"), "minExclusive must reject its endpoint");
assert!(accepts(&s, "2"));
assert!(accepts(&s, "9"));
assert!(!accepts(&s, "10"), "maxExclusive must reject its endpoint");
}
#[test]
fn a_bound_violation_names_the_bound() {
let s = restricted("xs:integer", r#"<xs:maxInclusive value="10"/>"#);
let text = report(&s, "11").to_string();
assert!(text.contains("10"), "{text}");
}
#[test]
fn a_pattern_facet_is_enforced() {
let s =
restricted("xs:string", r#"<xs:pattern value="[A-Z]{2}[0-9]{3}"/>"#);
assert!(accepts(&s, "AB123"));
assert!(!accepts(&s, "ab123"));
assert!(!accepts(&s, "AB12"));
assert!(!accepts(&s, "XAB123"), "patterns are anchored");
}
#[test]
fn facets_compose_and_every_failure_is_reported() {
let s = restricted(
"xs:string",
r#"<xs:minLength value="3"/><xs:pattern value="[a-z]+"/>"#,
);
assert!(accepts(&s, "abc"));
assert!(!accepts(&s, "ab"), "too short");
assert!(!accepts(&s, "ABC"), "wrong pattern");
}
#[test]
fn an_attribute_is_validated_against_its_type() {
let xsd = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="r">
<xs:complexType>
<xs:sequence/>
<xs:attribute name="n" type="xs:integer" use="required"/>
</xs:complexType>
</xs:element>
</xs:schema>"#;
let s = parse_schema(xsd).expect("valid schema");
let ok = oxml::parse(r#"<r n="42"/>"#).expect("well-formed");
assert!(validate(&ok, &s).violations.is_empty());
let bad = oxml::parse(r#"<r n="forty"/>"#).expect("well-formed");
assert!(!validate(&bad, &s).violations.is_empty());
let missing = oxml::parse("<r/>").expect("well-formed");
let report = validate(&missing, &s);
assert!(!report.violations.is_empty(), "required attribute absent");
assert!(report.to_string().contains('n'), "{report}");
}
#[test]
fn an_attribute_with_an_inline_simple_type_is_constrained() {
let xsd = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="r">
<xs:complexType>
<xs:sequence/>
<xs:attribute name="code">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="A"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>"#;
let s = parse_schema(xsd).expect("valid schema");
let ok = oxml::parse(r#"<r code="A"/>"#).expect("well-formed");
assert!(validate(&ok, &s).violations.is_empty());
let bad = oxml::parse(r#"<r code="B"/>"#).expect("well-formed");
assert!(!validate(&bad, &s).violations.is_empty());
}
#[test]
fn an_attribute_with_no_declared_type_accepts_anything() {
let xsd = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="r">
<xs:complexType>
<xs:sequence/>
<xs:attribute name="free"/>
</xs:complexType>
</xs:element>
</xs:schema>"#;
let s = parse_schema(xsd).expect("valid schema");
let doc = oxml::parse(r#"<r free="anything at all"/>"#).expect("ok");
assert!(validate(&doc, &s).violations.is_empty());
}
#[test]
fn an_unrecognised_base_type_falls_back_to_string() {
let s = restricted("xs:madeUpType", r#"<xs:minLength value="2"/>"#);
assert!(accepts(&s, "ab"));
assert!(!accepts(&s, "a"));
}