import Testing
@testable import PureXML
@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("TAB", type).isEmpty)
#expect(try !validate("AT", type).isEmpty)
#expect(try !validate("T", 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("", type).isEmpty)
#expect(try validate("", type).isEmpty)
#expect(try !validate("", 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("", type).isEmpty)
#expect(try validate("", type).isEmpty)
#expect(try !validate("", type).isEmpty)
#expect(try !validate("", 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("", type).isEmpty)
#expect(try !validate("", type).isEmpty) // missing required id
#expect(try !validate("", type).isEmpty) // id not an int
#expect(try !validate("", 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("42", type).isEmpty)
#expect(try !validate("x", type).isEmpty)
#expect(try !validate("", 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("text", elementOnly).isEmpty)
#expect(try validate("text", 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("3", type).isEmpty)
#expect(try !validate("-1", type).isEmpty)
}
}