PureXML/Tests/XSDIdentityTests.swift
Mihaela Mihaljevic 74b9332c48 fix(schema): reject identity field whose declared target is a non-simple complex type
cvc-identity-constraint.3 / c-fields-xpaths require an identity-constraint field to
identify a node with a simple type. The prior fix rejected a field whose selected
instance node carries element children (structural). This adds the case where the
field's DECLARED type is a complex type with non-simple content (empty or
element-only/mixed): an attributes-only element resolves to an empty-content complex
type, so its instance node is childless yet not simple-typed.

The field's target type is resolved through the schema type model (the selector
target's declared type, then the field's same-named child element in its content
model) and classified. A simple or simpleContent target, a type reached only
through a named reference, a complex field path, or an unresolvable target is left
alone, so no valid field is rejected. The resolution helpers live in a separate
extension file to keep the validator within the type-body limit.

This catches W3C XSTS idG006. XSTS invalid-instances-accepted 23 -> 22,
valid-instances-rejected held at 0.
2026-06-22 13:10:30 +02:00

248 lines
11 KiB
Swift

import Testing
@testable import PureXML
@Suite("XSD identity constraints")
struct XSDIdentityTests {
private func validate(_ xsd: String, _ xml: String) throws -> [PureXML.Validation.ValidationError] {
try PureXML.Schema.Document(xsd).validate(xml)
}
private let head = "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">"
@Test("xs:unique rejects a duplicated field value")
func test_unique() throws {
let xsd = """
\(head)
<xs:element name="list">
<xs:complexType>
<xs:sequence><xs:element name="item" maxOccurs="unbounded"><xs:complexType><xs:attribute name="id" type="xs:string"/></xs:complexType></xs:element></xs:sequence>
</xs:complexType>
<xs:unique name="byId">
<xs:selector xpath="item"/>
<xs:field xpath="@id"/>
</xs:unique>
</xs:element>
</xs:schema>
"""
#expect(try validate(xsd, "<list><item id=\"1\"/><item id=\"2\"/></list>").isEmpty)
#expect(try !validate(xsd, "<list><item id=\"1\"/><item id=\"1\"/></list>").isEmpty)
// unique tolerates a missing field
#expect(try validate(xsd, "<list><item/><item/></list>").isEmpty)
}
@Test("Streaming validation reports identity constraints like the tree path")
func test_streamingIdentity() throws {
let xsd = """
\(head)
<xs:element name="list">
<xs:complexType>
<xs:sequence><xs:element name="item" maxOccurs="unbounded"><xs:complexType><xs:attribute name="id" type="xs:string"/></xs:complexType></xs:element></xs:sequence>
</xs:complexType>
<xs:unique name="byId">
<xs:selector xpath="item"/>
<xs:field xpath="@id"/>
</xs:unique>
</xs:element>
</xs:schema>
"""
let xml = "<list><item id=\"1\"/><item id=\"1\"/></list>"
let document = try PureXML.Schema.Document(xsd)
let tree = try document.validate(xml).map(\.reason).sorted()
let streamed = try document.validate(streaming: xml).map(\.reason).sorted()
#expect(tree == streamed)
#expect(!streamed.isEmpty)
}
@Test("xs:key requires every field to be present and distinct")
func test_key() throws {
let xsd = """
\(head)
<xs:element name="list">
<xs:complexType>
<xs:sequence><xs:element name="item" maxOccurs="unbounded"><xs:complexType><xs:attribute name="id" type="xs:string"/></xs:complexType></xs:element></xs:sequence>
</xs:complexType>
<xs:key name="itemKey">
<xs:selector xpath="item"/>
<xs:field xpath="@id"/>
</xs:key>
</xs:element>
</xs:schema>
"""
#expect(try validate(xsd, "<list><item id=\"1\"/><item id=\"2\"/></list>").isEmpty)
#expect(try !validate(xsd, "<list><item id=\"1\"/><item id=\"1\"/></list>").isEmpty)
// a key field may not be absent
#expect(try !validate(xsd, "<list><item id=\"1\"/><item/></list>").isEmpty)
}
@Test("xs:keyref requires a matching key value")
func test_keyref() throws {
let xsd = """
\(head)
<xs:element name="orders">
<xs:complexType>
<xs:sequence>
<xs:element name="product" maxOccurs="unbounded"><xs:complexType><xs:attribute name="code" type="xs:string"/></xs:complexType></xs:element>
<xs:element name="line" maxOccurs="unbounded"><xs:complexType><xs:attribute name="product" type="xs:string"/></xs:complexType></xs:element>
</xs:sequence>
</xs:complexType>
<xs:key name="prodKey">
<xs:selector xpath="product"/>
<xs:field xpath="@code"/>
</xs:key>
<xs:keyref name="lineRef" refer="prodKey">
<xs:selector xpath="line"/>
<xs:field xpath="@product"/>
</xs:keyref>
</xs:element>
</xs:schema>
"""
let valid = "<orders><product code=\"A\"/><product code=\"B\"/><line product=\"A\"/></orders>"
let dangling = "<orders><product code=\"A\"/><line product=\"Z\"/></orders>"
#expect(try validate(xsd, valid).isEmpty)
// The dangling keyref is located at the offending field @product.
let failure = try #require(validate(xsd, dangling).first)
#expect(failure.codingPath.map(\.stringValue) == ["orders", "line", "@product"])
#expect(String(describing: failure).hasSuffix("at path: orders/line/@product"))
}
@Test("Identity-constraint errors are located at the offending field")
func test_identityErrorsCarryPath() throws {
let xsd = """
\(head)
<xs:element name="list">
<xs:complexType>
<xs:sequence><xs:element name="item" maxOccurs="unbounded"><xs:complexType><xs:attribute name="id" type="xs:string"/></xs:complexType></xs:element></xs:sequence>
</xs:complexType>
<xs:key name="itemKey">
<xs:selector xpath="item"/>
<xs:field xpath="@id"/>
</xs:key>
</xs:element>
</xs:schema>
"""
// The error locates the offending field (the duplicate item[2]'s @id),
// not just the element that declares the key.
let errors = try validate(xsd, "<list><item id=\"1\"/><item id=\"1\"/></list>")
let failure = try #require(errors.first)
#expect(failure.codingPath.map(\.stringValue) == ["list", "item", "@id"])
#expect(String(describing: failure).hasSuffix("at path: list/item[2]/@id"))
}
@Test("A nested selector locates the offender at full depth with an intermediate index")
func test_identityErrorDeepPath() throws {
let xsd = """
\(head)
<xs:element name="list">
<xs:complexType>
<xs:sequence>
<xs:element name="group" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded"><xs:complexType><xs:attribute name="id" type="xs:string"/></xs:complexType></xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:key name="k"><xs:selector xpath="group/item"/><xs:field xpath="@id"/></xs:key>
</xs:element>
</xs:schema>
"""
// The duplicate id "1" appears in the second group, so the offender is
// list/group[2]/item: a grandchild, with the index on the intermediate group.
let xml = "<list><group><item id=\"1\"/></group><group><item id=\"1\"/></group></list>"
let failure = try #require(validate(xsd, xml).first)
#expect(failure.codingPath.map(\.stringValue) == ["list", "group", "item", "@id"])
#expect(String(describing: failure).hasSuffix("at path: list/group[2]/item/@id"))
}
@Test("A keyref resolves against a key declared on an ancestor (cross-scope)")
func test_keyrefCrossScope() throws {
// pk is declared on <catalog>; the keyref is declared on the deeper
// <orders>, so resolution must find the ancestor's key in scope.
let xsd = """
\(head)
<xs:element name="catalog">
<xs:complexType><xs:sequence>
<xs:element name="products"><xs:complexType><xs:sequence>
<xs:element name="product" maxOccurs="unbounded"><xs:complexType><xs:attribute name="code" type="xs:string"/></xs:complexType></xs:element>
</xs:sequence></xs:complexType></xs:element>
<xs:element name="orders"><xs:complexType><xs:sequence>
<xs:element name="line" maxOccurs="unbounded"><xs:complexType><xs:attribute name="ref" type="xs:string"/></xs:complexType></xs:element>
</xs:sequence></xs:complexType>
<xs:keyref name="kr" refer="pk"><xs:selector xpath="line"/><xs:field xpath="@ref"/></xs:keyref>
</xs:element>
</xs:sequence></xs:complexType>
<xs:key name="pk"><xs:selector xpath="products/product"/><xs:field xpath="@code"/></xs:key>
</xs:element>
</xs:schema>
"""
let valid = "<catalog><products><product code=\"A\"/></products><orders><line ref=\"A\"/></orders></catalog>"
let dangling = "<catalog><products><product code=\"A\"/></products><orders><line ref=\"Z\"/></orders></catalog>"
#expect(try validate(xsd, valid).isEmpty)
#expect(try validate(xsd, dangling).contains { $0.reason.contains("no matching key 'pk'") })
}
@Test("A malformed field XPath is rejected at compile time, not silently disabled")
func test_malformedFieldReported() {
let xsd = """
\(head)
<xs:element name="list">
<xs:complexType>
<xs:sequence><xs:element name="item" maxOccurs="unbounded"><xs:complexType><xs:attribute name="id" type="xs:string"/></xs:complexType></xs:element></xs:sequence>
</xs:complexType>
<xs:key name="k"><xs:selector xpath="item"/><xs:field xpath="@id["/></xs:key>
</xs:element>
</xs:schema>
"""
#expect(rejectsIdentityXPath(xsd), "a field xpath with a predicate bracket is not in the XSD subset")
}
@Test("A malformed selector XPath is rejected at compile time")
func test_malformedSelectorReported() {
let xsd = """
\(head)
<xs:element name="list">
<xs:complexType>
<xs:sequence><xs:element name="item" maxOccurs="unbounded"><xs:complexType><xs:attribute name="id" type="xs:string"/></xs:complexType></xs:element></xs:sequence>
</xs:complexType>
<xs:key name="k"><xs:selector xpath="item["/><xs:field xpath="@id"/></xs:key>
</xs:element>
</xs:schema>
"""
#expect(rejectsIdentityXPath(xsd), "a selector xpath with a predicate bracket is not in the XSD subset")
}
/// Whether compiling `xsd` is rejected for an out-of-subset identity-constraint
/// XPath (the malformed path is caught at compile time, not silently disabled).
private func rejectsIdentityXPath(_ xsd: String) -> Bool {
do {
_ = try PureXML.Schema.Document(xsd)
return false
} catch {
return "\(error)".contains("is not a valid identity-constraint path")
}
}
@Test("A child-element field locates the error at that child")
func test_childElementField() throws {
let xsd = """
\(head)
<xs:element name="list">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded">
<xs:complexType><xs:sequence><xs:element name="code" type="xs:string"/></xs:sequence></xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:key name="k"><xs:selector xpath="item"/><xs:field xpath="code"/></xs:key>
</xs:element>
</xs:schema>
"""
let xml = "<list><item><code>x</code></item><item><code>x</code></item></list>"
let failure = try #require(validate(xsd, xml).first)
#expect(String(describing: failure).hasSuffix("at path: list/item[2]/code"))
}
}