PureXML/Tests/StreamingXSDTests.swift
Mihaela Mihaljevic a7f5407552 feat(validation): streaming XSD validation, completing #107
Add Schema.Document.validate(streaming:) and ComplexValidator.validateShallow:
resolve each element's type from its parent's content model (and xsi:type /
typeReference) as it opens, then check its attributes and content-model
structure shallowly as it closes, against a shallow element synthesized from
the streamed child names. A child's deeper validity is checked when it closes,
so only the open-element stack is held. A differential test asserts streaming
agrees with the tree validator across valid and invalid documents (missing
attributes, bad simple values, missing/extra/misordered children). Wildcard
processContents and substitution-group resolution in the streaming form stay
on the tree validator. The streaming methods moved to ComplexValidatorStreaming
to keep ComplexValidator under the file/type-body caps.

Closes #107
2026-06-09 16:52:17 +02:00

71 lines
3.4 KiB
Swift

@testable import PureXML
import Testing
@Suite("Streaming XSD validation")
struct StreamingXSDTests {
private let schema = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="order">
<xs:complexType>
<xs:sequence>
<xs:element name="id" type="xs:int"/>
<xs:element name="item" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="qty" type="xs:positiveInteger"/>
</xs:sequence>
<xs:attribute name="sku" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="ref" type="xs:string"/>
</xs:complexType>
</xs:element>
</xs:schema>
"""
/// Streaming validation must agree with the tree validator on the same
/// document (these schemas carry no identity constraints, so the tree output
/// is exactly the content/attribute errors streaming produces). Agreement on
/// every document is the correctness proof.
private func check(_ xml: String, expectValid: Bool) throws {
let document = try PureXML.Schema.Document(schema)
let tree = try document.validate(xml).map(\.reason).sorted()
let streamed = try document.validate(streaming: xml).map(\.reason).sorted()
#expect(tree == streamed, "streaming disagreed with tree on: \(xml)\n tree: \(tree)\n stream: \(streamed)")
#expect(streamed.isEmpty == expectValid, "wrong verdict for: \(xml) -> \(streamed)")
}
@Test("Valid documents agree and pass")
func test_valid() throws {
try check("<order ref=\"r\"><id>1</id><item sku=\"a\"><name>x</name><qty>2</qty></item></order>", expectValid: true)
// Repeated item (maxOccurs unbounded), optional ref omitted.
try check(
"<order><id>1</id><item sku=\"a\"><name>x</name><qty>2</qty></item>"
+ "<item sku=\"b\"><name>y</name><qty>9</qty></item></order>",
expectValid: true,
)
}
@Test("Invalid documents agree and fail")
func test_invalid() throws {
// Missing required attribute sku.
try check("<order><id>1</id><item><name>x</name><qty>2</qty></item></order>", expectValid: false)
// A bad xs:int value for id.
try check("<order><id>notanint</id><item sku=\"a\"><name>x</name><qty>2</qty></item></order>", expectValid: false)
// A bad xs:positiveInteger value for qty.
try check("<order><id>1</id><item sku=\"a\"><name>x</name><qty>0</qty></item></order>", expectValid: false)
// Missing required child qty.
try check("<order><id>1</id><item sku=\"a\"><name>x</name></item></order>", expectValid: false)
// Children out of order.
try check("<order><id>1</id><item sku=\"a\"><qty>2</qty><name>x</name></item></order>", expectValid: false)
// An undeclared extra child.
try check("<order><id>1</id><item sku=\"a\"><name>x</name><qty>2</qty><extra/></item></order>", expectValid: false)
// An undeclared attribute.
try check("<order><id>1</id><item sku=\"a\" bogus=\"1\"><name>x</name><qty>2</qty></item></order>", expectValid: false)
// No items at all (item is required, maxOccurs unbounded but minOccurs 1).
try check("<order><id>1</id></order>", expectValid: false)
}
}