refactor(xsd): express the streaming content check as a Validation value

Rule-canon audit (validation-rules.md / the OpenAPIKit idiom): the streaming
XSD validator was the one surface applying a bare method (validateShallow)
rather than a Validation<> value. Wrap it: ComplexValidator.shallowValidity is
a Validation<ResolvedElement, ComplexValidator> whose subject carries the
element with its already-resolved effective type (the contextual typing a
node-tree walk cannot express), and the streaming driver applies it via
apply(to:at:in:). Now every validation surface, DTD, XSD, RELAX NG, Schematron,
HTML, structural, conformance, and both streaming validators, routes through a
Validation value. Adds a direct unit test of the rule.
This commit is contained in:
Mihaela Mihaljevic 2026-06-10 02:14:02 +02:00
parent 067aa91861
commit 4c93555d1f
4 changed files with 51 additions and 6 deletions

View File

@ -99,7 +99,7 @@ flowchart TB
PureXML is a working, dependency-free XML library today: parse, emit, validate,
query, and stream documents on macOS, Linux, Windows, and WASM. The test suite
currently runs **815 tests in 125 suites** (`swift test`).
currently runs **816 tests in 125 suites** (`swift test`).
### Shipped (libxml2-aligned surface)

View File

@ -1,10 +1,36 @@
extension PureXML.Schema.ComplexValidator {
public extension PureXML.Schema {
/// An element paired with its already-resolved effective type: the subject of
/// the streaming XSD content rule, so the contextual type resolution that a
/// node-tree walk cannot express is carried on the subject instead.
struct ResolvedElement: PureXML.Validation.Validatable {
public let element: PureXML.Model.Element
public let type: ElementType
public init(element: PureXML.Model.Element, type: ElementType) {
self.element = element
self.type = type
}
}
}
public extension PureXML.Schema.ComplexValidator {
// MARK: Streaming support
/// The streaming XSD content check as a composable ``PureXML/Validation`` value
/// (the OpenAPIKit idiom): an element is valid against its resolved type. The
/// validator is the document, the cross-cutting context that holds the type
/// tables, so the streaming path is rule-driven like every other validator
/// rather than a bare method call.
static var shallowValidity: PureXML.Validation.Validation<PureXML.Schema.ResolvedElement, PureXML.Schema.ComplexValidator> {
.init(description: "Each streamed element is valid against its declared XSD type") { context in
context.document.validateShallow(context.subject.element, as: context.subject.type, at: context.codingPath)
}
}
/// The effective element type after an `xsi:type` override and following any
/// `typeReference` chain, so the streaming driver resolves a type once per
/// element rather than re-entering the tree walk.
public func effectiveType(_ declared: PureXML.Schema.ElementType, of element: PureXML.Model.Element) -> PureXML.Schema.ElementType {
func effectiveType(_ declared: PureXML.Schema.ElementType, of element: PureXML.Model.Element) -> PureXML.Schema.ElementType {
var type = declared
if let overriding = Self.xsiTypeName(element), let resolved = types[overriding] {
type = resolved
@ -20,7 +46,7 @@ extension PureXML.Schema.ComplexValidator {
/// The declared element type of a child `name` in `parent`'s content model, or
/// nil when the parent has no element content or no such child (an undeclared
/// child is flagged by the parent's own structure check).
public func childType(of parent: PureXML.Schema.ElementType, child name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? {
func childType(of parent: PureXML.Schema.ElementType, child name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? {
guard case let .complex(complex) = parent else { return nil }
switch complex.content {
case let .elementOnly(particle), let .mixed(particle):
@ -34,7 +60,7 @@ extension PureXML.Schema.ComplexValidator {
/// names, order, occurrence) and simple content, but not its children's own
/// validity, which the streaming driver checks as each child closes. `type`
/// must already be effective (see ``effectiveType(_:of:)``).
public func validateShallow(
func validateShallow(
_ element: PureXML.Model.Element,
as type: PureXML.Schema.ElementType,
at path: [PureXML.Validation.PathKey] = [],

View File

@ -69,7 +69,10 @@ public extension PureXML.Validation {
private mutating func close() {
guard let frame = stack.popLast() else { return }
if let effective = frame.effective {
collected += validator.validateShallow(frame.synthesized(), as: effective, at: frame.path)
// Apply the streaming content check as a composable Validation value
// (the OpenAPIKit idiom), not a bare method call.
let subject = PureXML.Schema.ResolvedElement(element: frame.synthesized(), type: effective)
collected += PureXML.Schema.ComplexValidator.shallowValidity.apply(to: subject, at: frame.path, in: validator)
}
}

View File

@ -67,4 +67,20 @@ struct StreamingXSDTests {
// No items at all (item is required, maxOccurs unbounded but minOccurs 1).
try check("<order><id>1</id></order>", expectValid: false)
}
@Test("The streaming content check is a composable Validation value")
func test_shallowValidityRule() {
// The streaming content check applied directly through the Validation rule.
let validator = PureXML.Schema.ComplexValidator()
let rule = PureXML.Schema.ComplexValidator.shallowValidity
#expect(rule.description == "Each streamed element is valid against its declared XSD type")
// An empty-content element carrying a child element is invalid.
let bad = PureXML.Schema.ResolvedElement(
element: PureXML.Model.Element("v", children: [.element(.init("child"))]),
type: .complex(PureXML.Schema.ComplexType(content: .empty)),
)
let errors = rule.apply(to: bad, at: [.element("v")], in: validator)
#expect(errors.contains { $0.reason.contains("must be empty") })
#expect(errors.first?.codingPath.map(\.stringValue) == ["v"])
}
}