use std::fmt::Write as _;
use xmlschema::{parse_schema, validate};
fn schema(body: &str) -> String {
format!(
r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">{body}</xs:schema>"#
)
}
fn valid(body: &str, xml: &str) -> bool {
let s = parse_schema(&schema(body)).expect("schema parses");
let doc = oxml::parse(xml).expect("well-formed");
validate(&doc, &s).is_valid()
}
#[test]
fn an_element_ref_reuses_the_top_level_declaration() {
let body = r#"
<xs:element name="shared" type="xs:integer"/>
<xs:element name="r">
<xs:complexType><xs:sequence>
<xs:element ref="shared"/>
</xs:sequence></xs:complexType>
</xs:element>"#;
assert!(valid(body, "<r><shared>42</shared></r>"));
assert!(!valid(body, "<r><shared>not a number</shared></r>"));
assert!(!valid(body, "<r></r>"), "still required");
}
#[test]
fn a_ref_carries_its_own_cardinality() {
let body = r#"
<xs:element name="item" type="xs:string"/>
<xs:element name="r">
<xs:complexType><xs:sequence>
<xs:element ref="item" minOccurs="0" maxOccurs="2"/>
</xs:sequence></xs:complexType>
</xs:element>"#;
assert!(valid(body, "<r></r>"));
assert!(valid(body, "<r><item>a</item><item>b</item></r>"));
assert!(
!valid(body, "<r><item>a</item><item>b</item><item>c</item></r>"),
"three exceeds maxOccurs"
);
}
#[test]
fn a_named_group_is_spliced_in() {
let body = r#"
<xs:group name="pair">
<xs:sequence>
<xs:element name="a" type="xs:string"/>
<xs:element name="b" type="xs:integer"/>
</xs:sequence>
</xs:group>
<xs:element name="r">
<xs:complexType><xs:sequence>
<xs:group ref="pair"/>
</xs:sequence></xs:complexType>
</xs:element>"#;
assert!(valid(body, "<r><a>x</a><b>1</b></r>"));
assert!(!valid(body, "<r><a>x</a><b>no</b></r>"), "b is an integer");
assert!(!valid(body, "<r><a>x</a></r>"), "b is required");
}
#[test]
fn a_named_attribute_group_is_spliced_in() {
let body = r#"
<xs:attributeGroup name="common">
<xs:attribute name="id" type="xs:integer" use="required"/>
<xs:attribute name="note" type="xs:string"/>
</xs:attributeGroup>
<xs:element name="r">
<xs:complexType>
<xs:attributeGroup ref="common"/>
</xs:complexType>
</xs:element>"#;
assert!(valid(body, r#"<r id="1"/>"#));
assert!(valid(body, r#"<r id="1" note="x"/>"#));
assert!(!valid(body, "<r/>"), "id is required");
assert!(!valid(body, r#"<r id="x"/>"#), "id is an integer");
}
#[test]
fn an_attribute_ref_reuses_the_top_level_declaration() {
let body = r#"
<xs:attribute name="when" type="xs:date"/>
<xs:element name="r">
<xs:complexType>
<xs:attribute ref="when" use="required"/>
</xs:complexType>
</xs:element>"#;
assert!(valid(body, r#"<r when="2001-01-01"/>"#));
assert!(!valid(body, r#"<r when="nonsense"/>"#));
assert!(!valid(body, "<r/>"), "the use here says required");
}
#[test]
fn complex_content_extension_appends_to_the_base() {
let body = r#"
<xs:complexType name="base">
<xs:sequence><xs:element name="a" type="xs:string"/></xs:sequence>
</xs:complexType>
<xs:complexType name="derived">
<xs:complexContent>
<xs:extension base="base">
<xs:sequence><xs:element name="b" type="xs:integer"/></xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:element name="r" type="derived"/>"#;
assert!(
valid(body, "<r><a>x</a><b>1</b></r>"),
"base then extension"
);
assert!(
!valid(body, "<r><a>x</a></r>"),
"b comes with the extension"
);
assert!(!valid(body, "<r><b>1</b></r>"), "a comes from the base");
}
#[test]
fn complex_content_restriction_states_its_model_in_full() {
let body = r#"
<xs:complexType name="base">
<xs:sequence>
<xs:element name="a" type="xs:string"/>
<xs:element name="b" type="xs:string" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="narrow">
<xs:complexContent>
<xs:restriction base="base">
<xs:sequence><xs:element name="a" type="xs:string"/></xs:sequence>
</xs:restriction>
</xs:complexContent>
</xs:complexType>
<xs:element name="r" type="narrow"/>"#;
assert!(valid(body, "<r><a>x</a></r>"));
assert!(
!valid(body, "<r><a>x</a><b>y</b></r>"),
"b is restricted away"
);
}
#[test]
fn attributes_are_inherited_through_an_extension() {
let body = r#"
<xs:complexType name="base">
<xs:attribute name="id" type="xs:integer" use="required"/>
</xs:complexType>
<xs:complexType name="derived">
<xs:complexContent>
<xs:extension base="base">
<xs:attribute name="extra" type="xs:string"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:element name="r" type="derived"/>"#;
assert!(valid(body, r#"<r id="1" extra="x"/>"#));
assert!(
!valid(body, r#"<r extra="x"/>"#),
"id is inherited and required"
);
assert!(!valid(body, r#"<r id="no"/>"#), "and still an integer");
}
#[test]
fn simple_content_extension_keeps_the_base_type() {
let body = r#"
<xs:element name="r">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:integer">
<xs:attribute name="unit" type="xs:string"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>"#;
assert!(valid(body, r#"<r unit="m">42</r>"#));
assert!(
!valid(body, r#"<r unit="m">wide</r>"#),
"the content is integer"
);
}
#[test]
fn a_fixed_element_value_is_enforced() {
let body = r#"<xs:element name="r" type="xs:string" fixed="only"/>"#;
assert!(valid(body, "<r>only</r>"));
assert!(!valid(body, "<r>other</r>"));
}
#[test]
fn a_prohibited_attribute_is_rejected() {
let body = r#"
<xs:element name="r">
<xs:complexType>
<xs:attribute name="gone" type="xs:string" use="prohibited"/>
</xs:complexType>
</xs:element>"#;
assert!(valid(body, "<r/>"));
assert!(!valid(body, r#"<r gone="x"/>"#));
}
#[test]
fn xsi_nil_stands_in_for_content_only_where_permitted() {
const NS: &str = r#"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance""#;
let nillable =
r#"<xs:element name="r" type="xs:integer" nillable="true"/>"#;
let plain = r#"<xs:element name="r" type="xs:integer"/>"#;
assert!(valid(nillable, &format!(r#"<r {NS} xsi:nil="true"/>"#)));
assert!(
!valid(plain, &format!(r#"<r {NS} xsi:nil="true"/>"#)),
"not nillable"
);
assert!(
!valid(nillable, &format!(r#"<r {NS} xsi:nil="true">1</r>"#)),
"a nilled element must be empty"
);
}
#[test]
fn a_union_accepts_any_member() {
let body = r#"
<xs:element name="r">
<xs:simpleType>
<xs:union memberTypes="xs:integer xs:date"/>
</xs:simpleType>
</xs:element>"#;
assert!(valid(body, "<r>42</r>"));
assert!(valid(body, "<r>2001-01-01</r>"));
assert!(!valid(body, "<r>neither</r>"));
}
#[test]
fn a_union_may_nest_its_members() {
let body = r#"
<xs:element name="r">
<xs:simpleType>
<xs:union>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="yes"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType><xs:restriction base="xs:integer"/></xs:simpleType>
</xs:union>
</xs:simpleType>
</xs:element>"#;
assert!(valid(body, "<r>yes</r>"));
assert!(valid(body, "<r>7</r>"));
assert!(!valid(body, "<r>no</r>"));
}
#[test]
fn a_list_validates_every_item() {
let body = r#"
<xs:element name="r">
<xs:simpleType><xs:list itemType="xs:integer"/></xs:simpleType>
</xs:element>"#;
assert!(valid(body, "<r>1 2 3</r>"));
assert!(valid(body, "<r> 1 2 </r>"), "whitespace separates");
assert!(!valid(body, "<r>1 two 3</r>"));
}
#[test]
fn a_list_may_name_a_local_item_type() {
let body = r#"
<xs:simpleType name="small">
<xs:restriction base="xs:integer">
<xs:maxInclusive value="9"/>
</xs:restriction>
</xs:simpleType>
<xs:element name="r">
<xs:simpleType><xs:list itemType="small"/></xs:simpleType>
</xs:element>"#;
assert!(valid(body, "<r>1 2 9</r>"));
assert!(!valid(body, "<r>1 10</r>"), "the item facet applies");
}
#[test]
fn a_schema_that_would_expand_without_bound_is_refused() {
let doubling = |levels: usize| {
let mut body = String::from(
r#"<xs:complexType name="t0">
<xs:sequence><xs:element name="leaf" type="xs:string"/></xs:sequence>
</xs:complexType>"#,
);
for i in 1..=levels {
let prev = i - 1;
let _ = write!(
body,
r#"<xs:complexType name="t{i}"><xs:sequence>
<xs:element name="a" type="t{prev}"/>
<xs:element name="b" type="t{prev}"/>
</xs:sequence></xs:complexType>"#
);
}
let _ = write!(body, r#"<xs:element name="r" type="t{levels}"/>"#);
body
};
assert!(
parse_schema(&schema(&doubling(10))).is_ok(),
"a schema of ordinary size must still parse"
);
let e = parse_schema(&schema(&doubling(24)))
.expect_err("must be refused, not attempted");
assert!(
e.to_string().contains("particles"),
"the error should say what the limit was: {e}"
);
}