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
This commit is contained in:
parent
6f1ee204ff
commit
a7f5407552
@ -24,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- ISO-2022-JP decoding (#108, in progress): the byte decoder gains the WHATWG `iso-2022-jp` algorithm, selected by the `iso-2022-jp` declaration name. Unlike the other CJK encodings this one is stateful, escape sequences switch between ASCII, JIS X 0201 Roman (with the yen sign and overline), JIS X 0201 katakana, and the JIS X 0208 plane, which is resolved through the shared `jis0208` table (no new data). EUC-TW and the streaming form of ISO-2022-JP remain on the issue.
|
||||
- Streaming (reader-driven) DTD validation (#107, in progress): `PureXML.validate(streaming:dtd:)` and `PureXML.Validation.StreamingDTDValidator` validate a document against a DTD while it is pulled event by event (the libxml2 `xmlTextReader` validation model), holding only the open-element stack rather than the whole tree. Each element's content model and attributes are checked the moment it closes, by applying the exact per-element DTD rules to a shallow element synthesized from the streamed child names. Document-scoped ID/IDREF integrity is tracked in bounded sets (the identifier values, not the tree): a duplicate ID is reported at its repeat occurrence, and forward IDREFs resolve at the end of the stream. Streaming and tree validation report the same problems (a test asserts matching reasons), and errors carry a libxml2 `xmlGetNodePath`-style coding path. RELAX NG streaming is also available via `RelaxNG.validate(streaming:)`: the Brzozowski derivative is itself incremental, so it is driven directly from the event stream, retaining only the residual pattern (the `after`-threaded chain is the validation stack) and a light per-element frame for ignorable-whitespace handling; a differential test asserts it agrees with the tree engine across valid, invalid, whitespace, and mixed-content documents. XSD streaming remains on the issue.
|
||||
- Streaming (reader-driven) DTD validation (#107, in progress): `PureXML.validate(streaming:dtd:)` and `PureXML.Validation.StreamingDTDValidator` validate a document against a DTD while it is pulled event by event (the libxml2 `xmlTextReader` validation model), holding only the open-element stack rather than the whole tree. Each element's content model and attributes are checked the moment it closes, by applying the exact per-element DTD rules to a shallow element synthesized from the streamed child names. Document-scoped ID/IDREF integrity is tracked in bounded sets (the identifier values, not the tree): a duplicate ID is reported at its repeat occurrence, and forward IDREFs resolve at the end of the stream. Streaming and tree validation report the same problems (a test asserts matching reasons), and errors carry a libxml2 `xmlGetNodePath`-style coding path. RELAX NG streaming is also available via `RelaxNG.validate(streaming:)`: the Brzozowski derivative is itself incremental, so it is driven directly from the event stream, retaining only the residual pattern (the `after`-threaded chain is the validation stack) and a light per-element frame for ignorable-whitespace handling; a differential test asserts it agrees with the tree engine across valid, invalid, whitespace, and mixed-content documents. XSD streaming is available via `Schema.Document.validate(streaming:)`: each element's type is resolved from its parent's content model (and `xsi:type` / `typeReference`) as it opens, and its attributes and content-model structure are checked shallowly as it closes (`ComplexValidator.validateShallow`), with a differential test asserting agreement with the tree validator. Wildcard `processContents` and substitution-group resolution in the streaming form stay on the tree validator.
|
||||
- Output encoding (#106): `Serializer.serialize(_:encoding:)` and `PureXML.serialize(_:encoding:)` now emit bytes in a chosen encoding (the libxml2 `xmlSaveToFilename(..., encoding)` model), not only UTF-8. A new `PureXML.Parsing.ByteEncoder` covers the Unicode transformation formats, the single-byte legacy families (ISO-8859, Windows code pages, KOI8), and the multi-byte CJK encodings (Shift-JIS, EUC-JP, EUC-KR, GBK, GB18030, Big5). The CJK and single-byte inverses are built by running the existing, tested decoders over every valid byte sequence and recording the shortest sequence per scalar, so each encoder is by construction the exact inverse of its decoder (no new data, no separately maintained encode tables); GB18030's astral range uses the four-byte pointer formula. The declaration carries the encoding's canonical name, a UTF-16/32 stream is preceded by a byte-order mark, and any scalar the target encoding cannot represent is written as a decimal numeric character reference, so output always round-trips.
|
||||
- Big5 (#103): the byte decoder gains the WHATWG `big5` algorithm, for both the whole-buffer and streaming decoders, selected by the `big5` declaration name (and the `big5-hkscs` / `cn-big5` aliases). A lead in `0x81`-`0xFE` followed by a trail in `0x40`-`0x7E` or `0xA1`-`0xFE` resolves through the index, covering Traditional Chinese and the HKSCS extensions including the astral CJK ideographs (Extension B and the Compatibility Supplement). Four pointers decode to a base letter plus a combining mark (a two-scalar sequence). The 18,590-entry index is vendored verbatim from the WHATWG `index-big5.txt`. Big5 completes the major CJK families libxml2 reaches through iconv (Japanese, Korean, Simplified and Traditional Chinese).
|
||||
- Full GB18030 (#102): the byte decoder gains the GB18030 four-byte sequences on top of the GBK two-byte subset, for both the whole-buffer and streaming decoders, selected by the `gb18030` declaration name. A lead in `0x81`-`0xFE` followed by a digit opens a four-byte form whose pointer is mapped through the WHATWG `index-gb18030-ranges` linear ranges, reaching the supplementary planes (the `189000` range maps to `U+10000` and beyond). The 207 ranges are vendored verbatim from the WHATWG `index-gb18030-ranges.txt`. GB18030 is China's mandatory standard, supported in libxml2 through iconv.
|
||||
|
||||
@ -86,7 +86,7 @@ flowchart TB
|
||||
Epic74 --> Done
|
||||
Epic105["#105 remaining libxml2 parity frontier"]:::epic
|
||||
OutEnc["#106 output encodings: complete"]:::done
|
||||
StreamVal["#107 streaming validation: DTD and RELAX NG done; XSD remains"]:::todo
|
||||
StreamVal["#107 streaming validation: DTD, RELAX NG, XSD"]:::done
|
||||
MoreEnc["#108 encodings: ISO-2022-JP done; EUC-TW remains"]:::todo
|
||||
Depth["Tier 2 conformance corpora; Tier 3 EXSLT, DTD PE depth, C14N 1.1; frontier #104"]:::todo
|
||||
Epic105 --> OutEnc
|
||||
@ -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 **786 tests in 120 suites** (`swift test`).
|
||||
currently runs **788 tests in 121 suites** (`swift test`).
|
||||
|
||||
### Shipped (libxml2-aligned surface)
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/// File-scope aliases for the XSD complex-type validator, kept out of the
|
||||
/// namespace to avoid nesting a type two levels deep.
|
||||
private typealias XSDFailure = PureXML.Validation.ValidationError
|
||||
private typealias XSDPath = [PureXML.Validation.PathKey]
|
||||
typealias XSDFailure = PureXML.Validation.ValidationError
|
||||
typealias XSDPath = [PureXML.Validation.PathKey]
|
||||
private typealias XSDGroup = PureXML.Schema.Group
|
||||
private typealias XSDTerm = PureXML.Schema.Term
|
||||
private typealias XSDTermLabel = PureXML.Schema.TermLabel
|
||||
@ -65,7 +65,7 @@ public extension PureXML.Schema {
|
||||
/// The errors from an `xsi:nil="true"` element: rejecting it when the
|
||||
/// element is not nillable, or when it carries content. Returns nil when
|
||||
/// the element is not nilled.
|
||||
private func nilErrors(_ element: PureXML.Model.Element, at path: XSDPath) -> [XSDFailure]? {
|
||||
func nilErrors(_ element: PureXML.Model.Element, at path: XSDPath) -> [XSDFailure]? {
|
||||
guard Self.isNil(element) else { return nil }
|
||||
let name = element.name.localName
|
||||
if !nillableElements.contains(name) {
|
||||
@ -79,7 +79,7 @@ public extension PureXML.Schema {
|
||||
|
||||
/// The error from a `fixed` element value constraint: the element's text
|
||||
/// must equal the fixed value.
|
||||
private func elementFixedErrors(_ element: PureXML.Model.Element, at path: XSDPath) -> [XSDFailure] {
|
||||
func elementFixedErrors(_ element: PureXML.Model.Element, at path: XSDPath) -> [XSDFailure] {
|
||||
guard let fixed = elementConstraints[element.name.localName]?.fixedValue else { return [] }
|
||||
let text = Self.textContent(element)
|
||||
return text == fixed ? [] : [XSDFailure(reason: "element '\(element.name.localName)' is fixed and must be '\(fixed)'", at: path)]
|
||||
@ -99,7 +99,7 @@ public extension PureXML.Schema {
|
||||
|
||||
// MARK: Attributes
|
||||
|
||||
private func validateAttributes(
|
||||
func validateAttributes(
|
||||
_ element: PureXML.Model.Element,
|
||||
_ type: ComplexType,
|
||||
at path: XSDPath,
|
||||
@ -153,7 +153,7 @@ public extension PureXML.Schema {
|
||||
}
|
||||
}
|
||||
|
||||
private func rejectText(_ element: PureXML.Model.Element, at path: XSDPath, into errors: inout [XSDFailure]) {
|
||||
func rejectText(_ element: PureXML.Model.Element, at path: XSDPath, into errors: inout [XSDFailure]) {
|
||||
guard !Self.textContent(element).isEmpty else { return }
|
||||
errors.append(XSDFailure(reason: "element must not contain text", at: path))
|
||||
}
|
||||
@ -279,7 +279,7 @@ public extension PureXML.Schema {
|
||||
/// Located content-model diagnostics: pinpoint which child breaks the model and
|
||||
/// what was expected there, so an editor shows placed errors with recovery hints
|
||||
/// rather than one opaque "content does not match" per element.
|
||||
private extension PureXML.Schema.ComplexValidator {
|
||||
extension PureXML.Schema.ComplexValidator {
|
||||
/// Walks the children through the content automaton, flagging the first child
|
||||
/// the follow-set rejects, or the missing content when the sequence ends early.
|
||||
func sequenceStructureErrors(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
||||
|
||||
100
Sources/Schema/ComplexValidatorStreaming.swift
Normal file
100
Sources/Schema/ComplexValidatorStreaming.swift
Normal file
@ -0,0 +1,100 @@
|
||||
extension PureXML.Schema.ComplexValidator {
|
||||
// MARK: Streaming support
|
||||
|
||||
/// 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 {
|
||||
var type = declared
|
||||
if let overriding = Self.xsiTypeName(element), let resolved = types[overriding] {
|
||||
type = resolved
|
||||
}
|
||||
var followed = 0
|
||||
while case let .typeReference(name) = type, let resolved = types[name], followed < 64 {
|
||||
type = resolved
|
||||
followed += 1
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
/// 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? {
|
||||
guard case let .complex(complex) = parent else { return nil }
|
||||
switch complex.content {
|
||||
case let .elementOnly(particle), let .mixed(particle):
|
||||
return Self.elementTypes(in: particle.term)[Self.key(name)]
|
||||
case .empty, .simpleContent:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates one element's own attributes and content-model structure (child
|
||||
/// 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(
|
||||
_ element: PureXML.Model.Element,
|
||||
as type: PureXML.Schema.ElementType,
|
||||
at path: [PureXML.Validation.PathKey] = [],
|
||||
) -> [PureXML.Validation.ValidationError] {
|
||||
var errors: [XSDFailure] = []
|
||||
switch type {
|
||||
case let .simple(simple):
|
||||
validateSimpleElement(element, simple, at: path, into: &errors)
|
||||
case let .complex(complex):
|
||||
validateAttributes(element, complex, at: path, into: &errors)
|
||||
if let nilErrors = nilErrors(element, at: path) { return errors + nilErrors }
|
||||
shallowContent(element, complex.content, at: path, into: &errors)
|
||||
errors += elementFixedErrors(element, at: path)
|
||||
case let .typeReference(name):
|
||||
guard let resolved = types[name] else {
|
||||
return [XSDFailure(reason: "unknown type '\(name)'", at: path)]
|
||||
}
|
||||
return validateShallow(element, as: resolved, at: path)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
private func validateSimpleElement(_ element: PureXML.Model.Element, _ simple: PureXML.Schema.SimpleType, at path: XSDPath, into errors: inout [XSDFailure]) {
|
||||
if let nilErrors = nilErrors(element, at: path) {
|
||||
errors += nilErrors
|
||||
return
|
||||
}
|
||||
if !element.children.compactMap(\.element).isEmpty {
|
||||
errors.append(XSDFailure(reason: "'\(element.name.localName)' must not have children", at: path))
|
||||
}
|
||||
if let error = simple.validate(Self.textContent(element)) {
|
||||
errors.append(XSDFailure(reason: "'\(element.name.localName)': \(error)", at: path))
|
||||
}
|
||||
errors += elementFixedErrors(element, at: path)
|
||||
}
|
||||
|
||||
private func shallowContent(_ element: PureXML.Model.Element, _ content: PureXML.Schema.ContentType, at path: XSDPath, into errors: inout [XSDFailure]) {
|
||||
let children = element.children.compactMap(\.element)
|
||||
switch content {
|
||||
case .empty:
|
||||
if !children.isEmpty { errors.append(XSDFailure(reason: "element must be empty", at: path)) }
|
||||
rejectText(element, at: path, into: &errors)
|
||||
case let .simpleContent(type):
|
||||
if !children.isEmpty { errors.append(XSDFailure(reason: "element must not have children", at: path)) }
|
||||
if let error = type.validate(Self.textContent(element)) {
|
||||
errors.append(XSDFailure(reason: "content: \(error)", at: path))
|
||||
}
|
||||
case let .elementOnly(particle):
|
||||
rejectText(element, at: path, into: &errors)
|
||||
shallowStructure(particle, children: children, at: path, into: &errors)
|
||||
case let .mixed(particle):
|
||||
shallowStructure(particle, children: children, at: path, into: &errors)
|
||||
}
|
||||
}
|
||||
|
||||
private func shallowStructure(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
||||
if case let .group(group) = particle.term, group.compositor == .all {
|
||||
allStructureErrors(group, children: children, at: path, into: &errors)
|
||||
} else {
|
||||
sequenceStructureErrors(particle, children: children, at: path, into: &errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,6 +66,33 @@ public extension PureXML.Schema {
|
||||
try validate(PureXML.parse(xml))
|
||||
}
|
||||
|
||||
/// Validates `xml` against the schema while it is pulled event by event (the
|
||||
/// libxml2 `xmlTextReader` model) rather than over a built tree. Memory is
|
||||
/// bounded to the open-element stack. Content models, attributes, and simple
|
||||
/// content are checked; document-scoped identity constraints (which need the
|
||||
/// whole tree) stay on ``validate(_:)-(String)``.
|
||||
public func validate(streaming xml: String, limits: PureXML.Parsing.Limits = .default) throws -> [PureXML.Validation.ValidationError] {
|
||||
let validator = PureXML.Schema.ComplexValidator(
|
||||
types: types,
|
||||
nillableElements: nillableElements,
|
||||
elementConstraints: elementConstraints,
|
||||
abstractTypes: abstractTypes,
|
||||
typeBlock: typeBlock,
|
||||
typeDerivation: typeDerivation,
|
||||
)
|
||||
var driver = PureXML.Validation.StreamingXSDValidator(
|
||||
validator: validator,
|
||||
rootElements: elements,
|
||||
abstractElements: abstractElements,
|
||||
targetNamespace: targetNamespace,
|
||||
)
|
||||
var reader = PureXML.Parsing.EventReader(xml, limits: limits)
|
||||
while let event = try reader.next() {
|
||||
driver.consume(event)
|
||||
}
|
||||
return driver.finish()
|
||||
}
|
||||
|
||||
/// Validates an already-parsed node tree, so a caller can validate the same
|
||||
/// (possibly recovered) tree it is editing. See ``validate(_:)-(String)``.
|
||||
public func validate(_ node: PureXML.Model.Node) -> [PureXML.Validation.ValidationError] {
|
||||
|
||||
132
Sources/Schema/StreamingXSDValidator.swift
Normal file
132
Sources/Schema/StreamingXSDValidator.swift
Normal file
@ -0,0 +1,132 @@
|
||||
public extension PureXML.Validation {
|
||||
/// Validates a document against an XSD while it is pulled event by event (the
|
||||
/// libxml2 `xmlTextReader` validation model), holding only the open-element
|
||||
/// stack rather than the whole tree. Each element's type is resolved from its
|
||||
/// parent's content model as it opens, and its own attributes and content-model
|
||||
/// structure are checked the moment it closes, against a shallow element
|
||||
/// synthesized from the streamed child names and text. A child's deeper
|
||||
/// validity is checked when that child closes.
|
||||
///
|
||||
/// Type resolution covers the global root declaration, the parent content
|
||||
/// model, `xsi:type` overrides, and `typeReference` chains. Wildcard
|
||||
/// `processContents` and substitution groups stay on the tree validator.
|
||||
struct StreamingXSDValidator {
|
||||
private let validator: PureXML.Schema.ComplexValidator
|
||||
private let rootElements: [String: PureXML.Schema.ElementType]
|
||||
private let abstractElements: Set<String>
|
||||
private let targetNamespace: String?
|
||||
private var stack: [XSDStreamFrame] = []
|
||||
private var collected: [ValidationError] = []
|
||||
private var sawRoot = false
|
||||
|
||||
public init(
|
||||
validator: PureXML.Schema.ComplexValidator,
|
||||
rootElements: [String: PureXML.Schema.ElementType],
|
||||
abstractElements: Set<String> = [],
|
||||
targetNamespace: String? = nil,
|
||||
) {
|
||||
self.validator = validator
|
||||
self.rootElements = rootElements
|
||||
self.abstractElements = abstractElements
|
||||
self.targetNamespace = targetNamespace
|
||||
}
|
||||
|
||||
/// Feeds one parse event into the validator.
|
||||
public mutating func consume(_ event: PureXML.Parsing.Event) {
|
||||
switch event {
|
||||
case let .startElement(name, attributes):
|
||||
open(name, attributes)
|
||||
case let .characters(text), let .cdata(text):
|
||||
if !stack.isEmpty { stack[stack.count - 1].text += text }
|
||||
case .endElement:
|
||||
close()
|
||||
case .comment, .processingInstruction:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/// The complete error set. Call after the last event.
|
||||
public mutating func finish() -> [ValidationError] {
|
||||
if !sawRoot {
|
||||
collected.append(ValidationError(reason: "the document has no root element", at: []))
|
||||
}
|
||||
return collected
|
||||
}
|
||||
|
||||
// MARK: Internals
|
||||
|
||||
private mutating func open(_ name: PureXML.Model.QualifiedName, _ attributes: [PureXML.Model.Attribute]) {
|
||||
let path = openPath(name)
|
||||
let declared = resolveDeclared(name, path: path)
|
||||
if !stack.isEmpty {
|
||||
stack[stack.count - 1].childElements.append(PureXML.Model.Element(name: name))
|
||||
}
|
||||
let probe = PureXML.Model.Element(name: name, attributes: attributes)
|
||||
let effective = declared.map { validator.effectiveType($0, of: probe) }
|
||||
stack.append(XSDStreamFrame(name: name, attributes: attributes, effective: effective, path: path))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// The declared type for an opening element: the global declaration for the
|
||||
/// root (with the root-level prolog checks), or the parent content model's
|
||||
/// child type otherwise. Records prolog errors and returns nil when the
|
||||
/// element has no type to validate against.
|
||||
private mutating func resolveDeclared(_ name: PureXML.Model.QualifiedName, path: [PathKey]) -> PureXML.Schema.ElementType? {
|
||||
guard stack.isEmpty else {
|
||||
return stack.last?.effective.flatMap { validator.childType(of: $0, child: name) }
|
||||
}
|
||||
sawRoot = true
|
||||
guard let declaration = rootElements[name.localName] else {
|
||||
collected.append(ValidationError(reason: "no element declaration for '\(name.localName)'", at: path))
|
||||
return nil
|
||||
}
|
||||
if abstractElements.contains(name.localName) {
|
||||
collected.append(ValidationError(reason: "abstract element '\(name.localName)' must not appear in an instance", at: path))
|
||||
return nil
|
||||
}
|
||||
if let target = targetNamespace, !target.isEmpty, name.namespaceURI != target {
|
||||
collected.append(ValidationError(reason: "root element '\(name.localName)' is not in the schema target namespace '\(target)'", at: path))
|
||||
return nil
|
||||
}
|
||||
return declaration
|
||||
}
|
||||
|
||||
/// The coding path for an opening element: the root unindexed, a child with
|
||||
/// a one-based occurrence index (the `xmlGetNodePath` convention).
|
||||
private mutating func openPath(_ name: PureXML.Model.QualifiedName) -> [PathKey] {
|
||||
let key = name.description
|
||||
guard !stack.isEmpty else { return [.element(key)] }
|
||||
let parent = stack.count - 1
|
||||
stack[parent].childCounts[key, default: 0] += 1
|
||||
let index = stack[parent].childCounts[key] ?? 1
|
||||
return stack[parent].path + [.element(key, index: index)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One open element while streaming an XSD: its name, attributes, resolved type,
|
||||
/// coding path, and the direct-child placeholders and text accumulated until it
|
||||
/// closes. Only names and text are needed for the shallow content check.
|
||||
private struct XSDStreamFrame {
|
||||
let name: PureXML.Model.QualifiedName
|
||||
let attributes: [PureXML.Model.Attribute]
|
||||
let effective: PureXML.Schema.ElementType?
|
||||
let path: [PureXML.Validation.PathKey]
|
||||
var childElements: [PureXML.Model.Element] = []
|
||||
var childCounts: [String: Int] = [:]
|
||||
var text = ""
|
||||
|
||||
/// A shallow element reproducing what the content and attribute checks read:
|
||||
/// the direct child element names and the concatenated text.
|
||||
func synthesized() -> PureXML.Model.Element {
|
||||
let children: [PureXML.Model.Node] = childElements.map { .element($0) }
|
||||
+ (text.isEmpty ? [] : [.text(text)])
|
||||
return PureXML.Model.Element(name: name, attributes: attributes, children: children)
|
||||
}
|
||||
}
|
||||
70
Tests/StreamingXSDTests.swift
Normal file
70
Tests/StreamingXSDTests.swift
Normal file
@ -0,0 +1,70 @@
|
||||
@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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user