Complete the element-vs-element type clause of Particle Valid (Restriction): the restricting element's type must be, or derive by restriction from, the base element's type. Previously only names were compared, so an unrelated, wider, or ur-type element type was accepted under restriction. Enrich the content-model particle: Term.element carries the element type's derivation identity (the resolved type name, anyType for an absent type, nil for an inline anonymous type). Record named simpleType restriction derivations in the derivation table alongside complex ones. The element-vs-element check walks the user restriction chain and then the built-in lattice to decide derivation by name; an ur-type base, a union base (membership is not a restriction chain), and an inline anonymous type are permitted, so no valid restriction is rejected. XSTS: invalid schemas accepted 616 to 594, invalid instances accepted 165 to 160 (more invalid schemas rejected up front), valid schemas rejected held at 72, valid instances rejected held at 214.
114 lines
4.8 KiB
Swift
114 lines
4.8 KiB
Swift
@testable import PureXML
|
|
import Testing
|
|
|
|
@Suite("XSD complex types")
|
|
struct XSDComplexTypeTests {
|
|
private typealias Schema = PureXML.Schema
|
|
|
|
private func element(_ name: String, type: Schema.ElementType? = nil, min: Int = 1, max: Int? = 1) -> Schema.Particle {
|
|
Schema.Particle(minOccurs: min, maxOccurs: max, term: .element(name: .init(name), type: type, typeName: nil))
|
|
}
|
|
|
|
private func parse(_ xml: String) throws -> PureXML.Model.Element {
|
|
guard case let .document(children) = try PureXML.parse(xml), let root = children.compactMap(\.element).first else {
|
|
throw TestFailure.noRoot
|
|
}
|
|
return root
|
|
}
|
|
|
|
private enum TestFailure: Error { case noRoot }
|
|
|
|
private func validate(_ xml: String, _ type: Schema.ComplexType) throws -> [PureXML.Validation.ValidationError] {
|
|
try Schema.ComplexValidator().validate(parse(xml), against: type)
|
|
}
|
|
|
|
// MARK: Sequence and occurrence
|
|
|
|
@Test("A sequence accepts the declared order and occurrence")
|
|
func test_sequence() throws {
|
|
let type = Schema.ComplexType(content: .elementOnly(
|
|
Schema.Particle(term: .group(.init(compositor: .sequence, particles: [
|
|
element("title"),
|
|
element("author", min: 1, max: nil),
|
|
]))),
|
|
))
|
|
#expect(try validate("<book><title>T</title><author>A</author><author>B</author></book>", type).isEmpty)
|
|
#expect(try !validate("<book><author>A</author><title>T</title></book>", type).isEmpty)
|
|
#expect(try !validate("<book><title>T</title></book>", type).isEmpty)
|
|
}
|
|
|
|
@Test("A choice accepts exactly one alternative")
|
|
func test_choice() throws {
|
|
let type = Schema.ComplexType(content: .elementOnly(
|
|
Schema.Particle(term: .group(.init(compositor: .choice, particles: [element("a"), element("b")]))),
|
|
))
|
|
#expect(try validate("<r><a/></r>", type).isEmpty)
|
|
#expect(try validate("<r><b/></r>", type).isEmpty)
|
|
#expect(try !validate("<r><a/><b/></r>", type).isEmpty)
|
|
}
|
|
|
|
@Test("An all group accepts any order, each member once")
|
|
func test_all() throws {
|
|
let type = Schema.ComplexType(content: .elementOnly(
|
|
Schema.Particle(term: .group(.init(compositor: .all, particles: [
|
|
element("x"),
|
|
element("y", min: 0, max: 1),
|
|
]))),
|
|
))
|
|
#expect(try validate("<r><y/><x/></r>", type).isEmpty)
|
|
#expect(try validate("<r><x/></r>", type).isEmpty)
|
|
#expect(try !validate("<r><x/><x/></r>", type).isEmpty)
|
|
#expect(try !validate("<r><y/></r>", type).isEmpty)
|
|
}
|
|
|
|
// MARK: Attributes
|
|
|
|
@Test("Required and typed attributes are enforced")
|
|
func test_attributes() throws {
|
|
let type = Schema.ComplexType(
|
|
attributes: [
|
|
.init(name: .init("id"), type: .init(base: .int), required: true),
|
|
.init(name: .init("lang"), type: .init(base: .language)),
|
|
],
|
|
content: .empty,
|
|
)
|
|
#expect(try validate("<e id=\"1\" lang=\"en\"/>", type).isEmpty)
|
|
#expect(try !validate("<e/>", type).isEmpty) // missing required id
|
|
#expect(try !validate("<e id=\"x\"/>", type).isEmpty) // id not an int
|
|
#expect(try !validate("<e id=\"1\" extra=\"y\"/>", type).isEmpty) // undeclared attribute
|
|
}
|
|
|
|
// MARK: Simple and mixed content
|
|
|
|
@Test("Simple content validates the text against a simple type")
|
|
func test_simpleContent() throws {
|
|
let type = Schema.ComplexType(content: .simpleContent(.init(base: .int)))
|
|
#expect(try validate("<n>42</n>", type).isEmpty)
|
|
#expect(try !validate("<n>x</n>", type).isEmpty)
|
|
#expect(try !validate("<n><child/></n>", type).isEmpty)
|
|
}
|
|
|
|
@Test("Element-only content rejects stray text; mixed allows it")
|
|
func test_elementOnlyVsMixed() throws {
|
|
let particle = Schema.Particle(term: .group(.init(compositor: .sequence, particles: [element("a")])))
|
|
let elementOnly = Schema.ComplexType(content: .elementOnly(particle))
|
|
let mixed = Schema.ComplexType(content: .mixed(particle))
|
|
#expect(try !validate("<r>text<a/></r>", elementOnly).isEmpty)
|
|
#expect(try validate("<r>text<a/></r>", mixed).isEmpty)
|
|
}
|
|
|
|
// MARK: Recursion
|
|
|
|
@Test("Child elements are validated against their declared types")
|
|
func test_recursiveTypes() throws {
|
|
let count = Schema.ElementType.simple(.init(base: .nonNegativeInteger))
|
|
let type = Schema.ComplexType(content: .elementOnly(
|
|
Schema.Particle(term: .group(.init(compositor: .sequence, particles: [
|
|
element("count", type: count),
|
|
]))),
|
|
))
|
|
#expect(try validate("<r><count>3</count></r>", type).isEmpty)
|
|
#expect(try !validate("<r><count>-1</count></r>", type).isEmpty)
|
|
}
|
|
}
|