PureXML/Tests/SchemaFacetApplicabilityTests.swift
Mihaela Mihaljevic 0143381f67 feat(schema): enforce facet applicability to simple-type variety
A constraining facet must apply to its simple type's variety (XSD 1.0 Part 2):
a list may be constrained only by length/minLength/maxLength, pattern,
enumeration, whiteSpace; a union only by pattern and enumeration. A value-bound
facet (maxInclusive, totalDigits, ...) constrains an atomic value space, so
restricting a list of integer by maxInclusive was wrongly accepted.

The base's variety is resolved from the raw tree (this runs before the types
are compiled), namespace-aware so a user type sharing a built-in's local name
does not flip the variety: a base in the XSD namespace is a built-in (only the
list datatypes are list-variety), a base in the schema's own target namespace
resolves to a declared global, and an external or unresolvable base is skipped.

XSTS: invalid schemas accepted 664 to 638, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 11:36:21 +02:00

74 lines
3.5 KiB
Swift

@testable import PureXML
import Testing
/// Facet applicability to a simple type's variety (XSD 1.0 Part 2, 4.3): a `list`
/// admits only the length-family, `pattern`, `enumeration`, `whiteSpace`; a `union`
/// only `pattern`/`enumeration`. A value-bound facet on a list or union is invalid
/// (the stF families). An atomic, unresolvable, or external base is left alone.
@Suite("Facet applicability to variety")
struct SchemaFacetApplicabilityTests {
private func compile(_ body: String) throws {
_ = try PureXML.Schema.Document("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
\(body)
</xs:schema>
""")
}
private func rejects(_ body: String) -> Bool {
do { try compile(body)
return false
} catch { return true }
}
@Test("a value-bound facet on a list-derived type is rejected")
func test_listValueBoundFacet() {
for facet in ["maxInclusive", "maxExclusive", "minInclusive", "minExclusive", "totalDigits", "fractionDigits"] {
#expect(rejects(#"""
<xs:simpleType name="l"><xs:list itemType="xs:integer"/></xs:simpleType>
<xs:simpleType name="t"><xs:restriction base="l"><xs:\#(facet) value="5"/></xs:restriction></xs:simpleType>
"""#), "expected rejection of \(facet) on a list")
}
// A built-in list datatype (xs:NMTOKENS) is also a list.
#expect(rejects(#"<xs:simpleType name="t"><xs:restriction base="xs:NMTOKENS"><xs:maxLength value="3"/><xs:maxInclusive value="5"/></xs:restriction></xs:simpleType>"#))
}
@Test("length-family, pattern, enumeration, whiteSpace apply to a list")
func test_listAllowedFacets() throws {
try compile(#"""
<xs:simpleType name="l"><xs:list itemType="xs:integer"/></xs:simpleType>
<xs:simpleType name="t"><xs:restriction base="l">
<xs:minLength value="1"/><xs:maxLength value="9"/><xs:enumeration value="1 2 3"/>
</xs:restriction></xs:simpleType>
"""#)
}
@Test("only pattern and enumeration apply to a union")
func test_unionFacets() throws {
#expect(rejects(#"""
<xs:simpleType name="u"><xs:union memberTypes="xs:integer xs:string"/></xs:simpleType>
<xs:simpleType name="t"><xs:restriction base="u"><xs:maxLength value="3"/></xs:restriction></xs:simpleType>
"""#))
try compile(#"""
<xs:simpleType name="u"><xs:union memberTypes="xs:integer xs:string"/></xs:simpleType>
<xs:simpleType name="t"><xs:restriction base="u"><xs:pattern value="\d+"/><xs:enumeration value="1"/></xs:restriction></xs:simpleType>
"""#)
}
@Test("a value-bound facet on an atomic type is left alone")
func test_atomicUnaffected() throws {
try compile(#"<xs:simpleType name="t"><xs:restriction base="xs:integer"><xs:maxInclusive value="5"/></xs:restriction></xs:simpleType>"#)
}
@Test("variety resolution is namespace-aware: a same-named user type does not shadow a built-in")
func test_namespaceAwareResolution() throws {
// A user `simpleType` named `integer` (a list) must not be mistaken for the
// built-in `xs:integer` when it is the restriction base; the value-bound
// facet on the atomic built-in is valid.
try compile(#"""
<xs:simpleType name="integer"><xs:list itemType="xs:string"/></xs:simpleType>
<xs:simpleType name="t"><xs:restriction base="xs:integer"><xs:maxInclusive value="5"/></xs:restriction></xs:simpleType>
"""#)
}
}