PureXML/Tests/XSDLocatedContentTests.swift
Mihaela Mihaljevic 68273205f6 feat(schema): full NameAndTypeOK type derivation in element restriction
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.
2026-06-14 13:47:18 +02:00

61 lines
3.0 KiB
Swift

@testable import PureXML
import Testing
@Suite("XSD located content-model errors")
struct XSDLocatedContentTests {
private typealias Schema = PureXML.Schema
private typealias Name = PureXML.Model.QualifiedName
private func element(_ name: String, children: [PureXML.Model.Node] = []) -> PureXML.Model.Element {
PureXML.Model.Element(name: Name(localName: name), attributes: [], children: children)
}
private func validate(_ element: PureXML.Model.Element, _ type: Schema.ComplexType) -> [PureXML.Validation.ValidationError] {
Schema.ComplexValidator().validate(element, against: type)
}
private func sequence(_ names: [String]) -> Schema.ComplexType {
let particles = names.map { Schema.Particle(term: .element(name: Name(localName: $0), type: nil, typeName: nil)) }
return Schema.ComplexType(content: .elementOnly(Schema.Particle(term: .group(Schema.Group(compositor: .sequence, particles: particles)))))
}
@Test("An unexpected child is located with the expected element named")
func test_unexpectedLocated() {
let type = sequence(["a", "b"])
let bad = element("root", children: [.element(element("a")), .element(element("x"))])
let failure = validate(bad, type).first
#expect(failure?.reason == "element 'x' is not allowed here; expected <b>")
#expect(failure?.codingPath.map(\.stringValue) == ["x"])
}
@Test("Missing required content reports what was expected, at the element")
func test_incompleteLocated() {
let type = sequence(["a", "b"])
let bad = element("root", children: [.element(element("a"))])
let failure = validate(bad, type).first
#expect(failure?.reason == "content is incomplete; expected <b>")
#expect(failure?.codingPath.isEmpty == true)
}
@Test("A valid sequence produces no content errors")
func test_valid() {
let type = sequence(["a", "b"])
let good = element("root", children: [.element(element("a")), .element(element("b"))])
#expect(validate(good, type).isEmpty)
}
@Test("An xs:all group reports every stray child and every missing member (recovery)")
func test_allRecovery() {
let members = ["a", "b"].map {
Schema.Particle(minOccurs: 1, maxOccurs: 1, term: .element(name: Name(localName: $0), type: nil, typeName: nil))
}
let type = Schema.ComplexType(content: .elementOnly(Schema.Particle(term: .group(Schema.Group(compositor: .all, particles: members)))))
// Two stray children, and member b never appears: three located problems.
let bad = element("root", children: [.element(element("x")), .element(element("y")), .element(element("a"))])
let reasons = validate(bad, type).map(\.reason)
#expect(reasons.contains { $0.contains("'x'") && $0.contains("not allowed") })
#expect(reasons.contains { $0.contains("'y'") && $0.contains("not allowed") })
#expect(reasons.contains { $0.contains("'b'") && $0.contains("required but missing") })
}
}