From 4c93555d1f55a4ef8adec9a0aa95fc89bf74ca89 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Wed, 10 Jun 2026 02:14:02 +0200 Subject: [PATCH] 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 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. --- README.md | 2 +- .../Schema/ComplexValidatorStreaming.swift | 34 ++++++++++++++++--- Sources/Schema/StreamingXSDValidator.swift | 5 ++- Tests/StreamingXSDTests.swift | 16 +++++++++ 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index be11f5b..45dc5e4 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/Sources/Schema/ComplexValidatorStreaming.swift b/Sources/Schema/ComplexValidatorStreaming.swift index 99a7e14..5bd28c5 100644 --- a/Sources/Schema/ComplexValidatorStreaming.swift +++ b/Sources/Schema/ComplexValidatorStreaming.swift @@ -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 { + .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] = [], diff --git a/Sources/Schema/StreamingXSDValidator.swift b/Sources/Schema/StreamingXSDValidator.swift index 26a05b5..ed9d36e 100644 --- a/Sources/Schema/StreamingXSDValidator.swift +++ b/Sources/Schema/StreamingXSDValidator.swift @@ -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) } } diff --git a/Tests/StreamingXSDTests.swift b/Tests/StreamingXSDTests.swift index af0a7c9..70bec19 100644 --- a/Tests/StreamingXSDTests.swift +++ b/Tests/StreamingXSDTests.swift @@ -67,4 +67,20 @@ struct StreamingXSDTests { // No items at all (item is required, maxOccurs unbounded but minOccurs 1). try check("1", 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"]) + } }