feat(xsd): add list/union simple types, attribute groups, and element/group refs

Extend the schema parser toward full structural parity:

- list simple types validate each whitespace-separated item against the item
  type and count items for the length facets; union simple types admit a value
  valid for any member type. A restriction over a named list or union inherits
  its variety.
- Named attribute groups are expanded (transitively) into every complex type
  that references them.
- Named model groups are expanded at each xs:group ref, with a cycle guard;
  element refs resolve to the referenced global element's declared type,
  through a reserved type-table key so forward and recursive refs settle at
  validation time.

Named simple types now resolve over repeated passes so they may restrict or
list one another regardless of document order. Simple-type and facet parsing
moves into a sibling XSDSimpleParser to keep each parser within bounds.

Closes #65
This commit is contained in:
Mihaela Mihaljevic 2026-06-08 08:28:17 +02:00
parent df9457d323
commit cadc63ba8b
3 changed files with 424 additions and 122 deletions

View File

@ -44,16 +44,38 @@ public extension PureXML.Schema {
}
}
/// A simple type: a built-in base restricted by facets. Validates a lexical
/// value against the XSD Part 2 rules: whitespace processing, the base value
/// space, the base's intrinsic bounds, then the facets.
/// A simple type's variety (XSD Part 2): atomic, a whitespace-separated list
/// of an item type, or a union of member types.
indirect enum Variety: Sendable {
case atomic
case list(SimpleType)
case union([SimpleType])
}
/// A simple type: a built-in base restricted by facets, of one of the three
/// varieties. Validates a lexical value against the XSD Part 2 rules:
/// whitespace processing, the base value space, the base's intrinsic bounds,
/// then the facets. A list checks each item against the item type and counts
/// items for the length facets; a union admits a value valid for any member.
struct SimpleType: Sendable {
public var base: BuiltinType
public var facets: Facets
public var variety: Variety
public init(base: BuiltinType, facets: Facets = Facets()) {
public init(base: BuiltinType, facets: Facets = Facets(), variety: Variety = .atomic) {
self.base = base
self.facets = facets
self.variety = variety
}
/// A list type whose items are `item`, carrying its own (list) facets.
public static func list(item: SimpleType, facets: Facets = Facets()) -> SimpleType {
SimpleType(base: .string, facets: facets, variety: .list(item))
}
/// A union of `members`: a value is valid if any member admits it.
public static func union(_ members: [SimpleType]) -> SimpleType {
SimpleType(base: .string, variety: .union(members))
}
/// Whether `lexical` is a valid value of this type.
@ -64,6 +86,37 @@ public extension PureXML.Schema {
/// A description of the first constraint `lexical` violates, or nil when it
/// is valid.
public func validate(_ lexical: String) -> String? {
switch variety {
case .atomic: validateAtomic(lexical)
case let .list(item): validateList(lexical, item: item)
case let .union(members): validateUnion(lexical, members: members)
}
}
private func validateList(_ lexical: String, item: SimpleType) -> String? {
let normalized = Self.process(lexical, whiteSpace: facets.whiteSpace ?? .collapse)
let tokens = normalized.isEmpty ? [] : normalized.split(separator: " ").map(String.init)
for token in tokens {
if let error = item.validate(token) { return error }
}
if let exact = facets.length, tokens.count != exact { return "list length \(tokens.count) is not \(exact)" }
if let minimum = facets.minLength, tokens.count < minimum { return "list length \(tokens.count) is below \(minimum)" }
if let maximum = facets.maxLength, tokens.count > maximum { return "list length \(tokens.count) is above \(maximum)" }
if let error = patternError(normalized) { return error }
if let enumeration = facets.enumeration, !enumeration.contains(normalized) {
return "'\(normalized)' is not in the enumeration"
}
return nil
}
private func validateUnion(_ lexical: String, members: [SimpleType]) -> String? {
for member in members where member.validate(lexical) == nil {
return nil
}
return "'\(lexical)' does not match any member type of the union"
}
private func validateAtomic(_ lexical: String) -> String? {
let value = Self.process(lexical, whiteSpace: facets.whiteSpace ?? base.whiteSpace)
let primitive = base.primitive
guard primitive.isValid(value) else {

View File

@ -34,13 +34,31 @@ private enum XSDNode {
}
}
/// The parsing context: the named simple types resolved so far, plus the
/// definition nodes for named attribute groups and model groups (so their refs
/// can be expanded), and a guard against cyclic group references. File-scope and
/// private.
private struct Context {
var simpleTypes: [String: PureXML.Schema.SimpleType]
var attributeGroups: [String: Tree]
var groups: [String: Tree]
var visitingGroups: Set<String> = []
func visiting(_ group: String) -> Context {
var copy = self
copy.visitingGroups.insert(group)
return copy
}
}
extension PureXML.Schema {
/// Parses an XSD schema document into its global element declarations and its
/// named-type table. Matches the schema vocabulary by local name, so the XML
/// Schema namespace prefix may be anything. Supports global and local element
/// declarations, named and inline simple and complex types, `sequence`,
/// `choice`, and `all` model groups with occurrence, attribute uses, simple
/// content, and the full facet set.
/// content, the full facet set, `list` and `union` simple types, named
/// attribute groups and model groups, and element and group references.
enum XSDParser {
static func parse(
_ xsd: String,
@ -49,38 +67,67 @@ extension PureXML.Schema {
guard let schema = XSDNode.elementChildren(root).first(where: { XSDNode.localName($0) == "schema" }) else {
throw PureXML.Schema.SchemaError.notASchema
}
var simpleTypes: [String: SimpleType] = [:]
for node in XSDNode.children(schema, named: "simpleType") {
if let name = XSDNode.attribute(node, "name") {
simpleTypes[name] = simpleType(node, simpleTypes)
var context = Context(
simpleTypes: [:],
attributeGroups: indexByName(XSDNode.children(schema, named: "attributeGroup")),
groups: indexByName(XSDNode.children(schema, named: "group")),
)
// Named simple types may restrict or list one another regardless of
// document order, so resolve them over repeated passes: each pass sees
// the types resolved by the previous one. A chain of length n settles
// in at most n passes.
let simpleNodes = XSDNode.children(schema, named: "simpleType").filter { XSDNode.attribute($0, "name") != nil }
for _ in simpleNodes.indices {
for node in simpleNodes {
if let name = XSDNode.attribute(node, "name") {
context.simpleTypes[name] = XSDSimpleParser.simpleType(node, context)
}
}
}
var types: [String: ElementType] = simpleTypes.mapValues(ElementType.simple)
var types: [String: ElementType] = context.simpleTypes.mapValues(ElementType.simple)
for node in XSDNode.children(schema, named: "complexType") {
if let name = XSDNode.attribute(node, "name") {
types[name] = .complex(complexType(node, simpleTypes))
types[name] = .complex(complexType(node, context))
}
}
var elements: [String: ElementType] = [:]
for node in XSDNode.children(schema, named: "element") {
if let name = XSDNode.attribute(node, "name") {
elements[name] = elementType(node, simpleTypes)
let type = elementType(node, context)
elements[name] = type
// An `xs:element ref="name"` resolves to this declaration's type
// through a reserved key (a colon keeps it out of the NCName-only
// type namespace), letting forward and recursive refs resolve at
// validation time.
types[elementKey(name)] = type
}
}
return (elements, types)
}
static func elementKey(_ name: String) -> String {
"element:\(name)"
}
private static func indexByName(_ nodes: [Tree]) -> [String: Tree] {
var index: [String: Tree] = [:]
for node in nodes {
if let name = XSDNode.attribute(node, "name") { index[name] = node }
}
return index
}
// MARK: Element and type references
private static func elementType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> ElementType {
private static func elementType(_ node: Tree, _ context: Context) -> ElementType {
if let typeName = XSDNode.attribute(node, "type") {
return typeReference(typeName)
}
if let inline = XSDNode.firstChild(node, named: "simpleType") {
return .simple(simpleType(inline, simpleTypes))
return .simple(XSDSimpleParser.simpleType(inline, context))
}
if let inline = XSDNode.firstChild(node, named: "complexType") {
return .complex(complexType(inline, simpleTypes))
return .complex(complexType(inline, context))
}
return .complex(anyType)
}
@ -93,12 +140,6 @@ extension PureXML.Schema {
return .typeReference(local)
}
private static func simpleTypeReference(_ typeName: String, _ simpleTypes: [String: SimpleType]) -> SimpleType {
let local = XSDNode.stripPrefix(typeName)
if let builtin = BuiltinType(rawValue: local) { return SimpleType(base: builtin) }
return simpleTypes[local] ?? SimpleType(base: .string)
}
private static var anyType: ComplexType {
ComplexType(
allowsOtherAttributes: true,
@ -106,26 +147,186 @@ extension PureXML.Schema {
)
}
// MARK: Simple types and facets
// MARK: Complex types
private static func simpleType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> SimpleType {
private static func complexType(_ node: Tree, _ context: Context) -> ComplexType {
let mixed = XSDNode.attribute(node, "mixed") == "true"
var attributes = attributeUses(under: node, context)
if let simpleContent = XSDNode.firstChild(node, named: "simpleContent") {
let inner = derivation(simpleContent)
attributes += inner.map { attributeUses(under: $0, context) } ?? []
return ComplexType(attributes: attributes, content: .simpleContent(simpleContentType(simpleContent, context)))
}
let container = XSDNode.firstChild(node, named: "complexContent").flatMap(derivation) ?? node
attributes += container === node ? [] : attributeUses(under: container, context)
guard let particle = modelGroup(in: container, context) else {
return ComplexType(attributes: attributes, content: .empty)
}
return ComplexType(attributes: attributes, content: mixed ? .mixed(particle) : .elementOnly(particle))
}
private static func derivation(_ node: Tree) -> Tree? {
XSDNode.firstChild(node, named: "restriction") ?? XSDNode.firstChild(node, named: "extension")
}
private static func simpleContentType(_ node: Tree, _ context: Context) -> SimpleType {
guard let inner = derivation(node) else { return SimpleType(base: .string) }
let baseName = XSDNode.stripPrefix(XSDNode.attribute(inner, "base") ?? "string")
let base = BuiltinType(rawValue: baseName) ?? context.simpleTypes[baseName]?.base ?? .string
var facets = context.simpleTypes[baseName]?.facets ?? Facets()
XSDSimpleParser.applyFacets(inner, into: &facets)
return SimpleType(base: base, facets: facets)
}
// MARK: Attribute uses and groups
private static func attributeUses(under node: Tree, _ context: Context, visited: Set<String> = []) -> [AttributeUse] {
var uses: [AttributeUse] = []
for child in XSDNode.elementChildren(node) {
switch XSDNode.localName(child) {
case "attribute":
if let use = attributeUse(child, context) { uses.append(use) }
case "attributeGroup":
guard let ref = XSDNode.attribute(child, "ref") else { break }
let name = XSDNode.stripPrefix(ref)
guard !visited.contains(name), let group = context.attributeGroups[name] else { break }
uses += attributeUses(under: group, context, visited: visited.union([name]))
default:
break
}
}
return uses
}
private static func attributeUse(_ node: Tree, _ context: Context) -> AttributeUse? {
guard let name = XSDNode.attribute(node, "name") else { return nil }
let required = XSDNode.attribute(node, "use") == "required"
let type: SimpleType = if let typeName = XSDNode.attribute(node, "type") {
XSDSimpleParser.simpleTypeReference(typeName, context)
} else if let inline = XSDNode.firstChild(node, named: "simpleType") {
XSDSimpleParser.simpleType(inline, context)
} else {
SimpleType(base: .string)
}
return AttributeUse(name: PureXML.Model.QualifiedName(name), type: type, required: required)
}
// MARK: Model groups
private static func modelGroup(in node: Tree, _ context: Context) -> Particle? {
for (name, compositor) in [("sequence", Compositor.sequence), ("choice", .choice), ("all", .all)] {
if let group = XSDNode.firstChild(node, named: name) {
return groupParticle(group, compositor, context)
}
}
if let groupRef = XSDNode.firstChild(node, named: "group") {
return particle(groupRef, context)
}
return nil
}
private static func groupParticle(
_ node: Tree,
_ compositor: Compositor,
_ context: Context,
) -> Particle {
var particles: [Particle] = []
for child in XSDNode.elementChildren(node) {
if let member = particle(child, context) { particles.append(member) }
}
let (minimum, maximum) = XSDNode.occurrence(node)
return Particle(
minOccurs: minimum,
maxOccurs: maximum,
term: .group(Group(compositor: compositor, particles: particles)),
)
}
private static func particle(_ node: Tree, _ context: Context) -> Particle? {
let (minimum, maximum) = XSDNode.occurrence(node)
switch XSDNode.localName(node) {
case "element":
return elementParticle(node, minimum, maximum, context)
case "sequence":
return groupParticle(node, .sequence, context)
case "choice":
return groupParticle(node, .choice, context)
case "all":
return groupParticle(node, .all, context)
case "group":
return groupReferenceParticle(node, minimum, maximum, context)
case "any":
return Particle(minOccurs: minimum, maxOccurs: maximum, term: .wildcard(Wildcard()))
default:
return nil
}
}
private static func elementParticle(_ node: Tree, _ minimum: Int, _ maximum: Int?, _ context: Context) -> Particle {
if let ref = XSDNode.attribute(node, "ref") {
let name = XSDNode.stripPrefix(ref)
return Particle(
minOccurs: minimum,
maxOccurs: maximum,
term: .element(name: PureXML.Model.QualifiedName(name), type: .typeReference(elementKey(name))),
)
}
let name = XSDNode.attribute(node, "name") ?? ""
return Particle(
minOccurs: minimum,
maxOccurs: maximum,
term: .element(name: PureXML.Model.QualifiedName(name), type: elementType(node, context)),
)
}
private static func groupReferenceParticle(_ node: Tree, _ minimum: Int, _ maximum: Int?, _ context: Context) -> Particle? {
guard let ref = XSDNode.attribute(node, "ref") else { return nil }
let name = XSDNode.stripPrefix(ref)
guard !context.visitingGroups.contains(name), let definition = context.groups[name],
let inner = modelGroup(in: definition, context.visiting(name))
else {
return nil
}
return Particle(minOccurs: minimum, maxOccurs: maximum, term: inner.term)
}
}
/// Parses XSD simple types: atomic restrictions with the full facet set, and
/// the `list` and `union` varieties. Kept beside ``XSDParser`` so the two
/// share the file-scope tree helpers and parsing context.
enum XSDSimpleParser {
fileprivate static func simpleType(_ node: Tree, _ context: Context) -> SimpleType {
if let list = XSDNode.firstChild(node, named: "list") {
return listType(list, context)
}
if let union = XSDNode.firstChild(node, named: "union") {
return unionType(union, context)
}
guard let restriction = XSDNode.firstChild(node, named: "restriction") else {
return SimpleType(base: .string)
}
let baseName = XSDNode.stripPrefix(XSDNode.attribute(restriction, "base") ?? "string")
var base = BuiltinType.string
var facets = Facets()
var variety = Variety.atomic
if let builtin = BuiltinType(rawValue: baseName) {
base = builtin
} else if let parent = simpleTypes[baseName] {
} else if let parent = context.simpleTypes[baseName] {
base = parent.base
facets = parent.facets
variety = parent.variety
}
applyFacets(restriction, into: &facets)
return SimpleType(base: base, facets: facets)
return SimpleType(base: base, facets: facets, variety: variety)
}
private static func applyFacets(_ restriction: Tree, into facets: inout Facets) {
fileprivate static func simpleTypeReference(_ typeName: String, _ context: Context) -> SimpleType {
let local = XSDNode.stripPrefix(typeName)
if let builtin = BuiltinType(rawValue: local) { return SimpleType(base: builtin) }
return context.simpleTypes[local] ?? SimpleType(base: .string)
}
fileprivate static func applyFacets(_ restriction: Tree, into facets: inout Facets) {
for facet in XSDNode.elementChildren(restriction) {
let value = XSDNode.attribute(facet, "value")
applyStringFacet(XSDNode.localName(facet), value, into: &facets)
@ -133,6 +334,26 @@ extension PureXML.Schema {
}
}
private static func listType(_ node: Tree, _ context: Context) -> SimpleType {
let item: SimpleType = if let itemType = XSDNode.attribute(node, "itemType") {
simpleTypeReference(itemType, context)
} else if let inline = XSDNode.firstChild(node, named: "simpleType") {
simpleType(inline, context)
} else {
SimpleType(base: .string)
}
return .list(item: item)
}
private static func unionType(_ node: Tree, _ context: Context) -> SimpleType {
var members: [SimpleType] = []
if let names = XSDNode.attribute(node, "memberTypes") {
members += names.split(whereSeparator: \.isWhitespace).map { simpleTypeReference(String($0), context) }
}
members += XSDNode.children(node, named: "simpleType").map { simpleType($0, context) }
return .union(members)
}
private static func applyStringFacet(_ name: String?, _ value: String?, into facets: inout Facets) {
switch name {
case "pattern": if let value { facets.patterns.append(value) }
@ -166,101 +387,5 @@ extension PureXML.Schema {
default: nil
}
}
// MARK: Complex types
private static func complexType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> ComplexType {
let mixed = XSDNode.attribute(node, "mixed") == "true"
var attributes = attributeUses(under: node, simpleTypes)
if let simpleContent = XSDNode.firstChild(node, named: "simpleContent") {
let inner = derivation(simpleContent)
attributes += inner.map { attributeUses(under: $0, simpleTypes) } ?? []
return ComplexType(attributes: attributes, content: .simpleContent(simpleContentType(simpleContent, simpleTypes)))
}
let container = XSDNode.firstChild(node, named: "complexContent").flatMap(derivation) ?? node
attributes += container === node ? [] : attributeUses(under: container, simpleTypes)
guard let particle = modelGroup(in: container, simpleTypes) else {
return ComplexType(attributes: attributes, content: .empty)
}
return ComplexType(attributes: attributes, content: mixed ? .mixed(particle) : .elementOnly(particle))
}
private static func derivation(_ node: Tree) -> Tree? {
XSDNode.firstChild(node, named: "restriction") ?? XSDNode.firstChild(node, named: "extension")
}
private static func simpleContentType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> SimpleType {
guard let inner = derivation(node) else { return SimpleType(base: .string) }
let baseName = XSDNode.stripPrefix(XSDNode.attribute(inner, "base") ?? "string")
let base = BuiltinType(rawValue: baseName) ?? simpleTypes[baseName]?.base ?? .string
var facets = simpleTypes[baseName]?.facets ?? Facets()
applyFacets(inner, into: &facets)
return SimpleType(base: base, facets: facets)
}
private static func attributeUses(under node: Tree, _ simpleTypes: [String: SimpleType]) -> [AttributeUse] {
XSDNode.children(node, named: "attribute").compactMap { attributeUse($0, simpleTypes) }
}
private static func attributeUse(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> AttributeUse? {
guard let name = XSDNode.attribute(node, "name") else { return nil }
let required = XSDNode.attribute(node, "use") == "required"
let type: SimpleType = if let typeName = XSDNode.attribute(node, "type") {
simpleTypeReference(typeName, simpleTypes)
} else if let inline = XSDNode.firstChild(node, named: "simpleType") {
simpleType(inline, simpleTypes)
} else {
SimpleType(base: .string)
}
return AttributeUse(name: PureXML.Model.QualifiedName(name), type: type, required: required)
}
// MARK: Model groups
private static func modelGroup(in node: Tree, _ simpleTypes: [String: SimpleType]) -> Particle? {
for (name, compositor) in [("sequence", Compositor.sequence), ("choice", .choice), ("all", .all)] {
if let group = XSDNode.firstChild(node, named: name) {
return groupParticle(group, compositor, simpleTypes)
}
}
return nil
}
private static func groupParticle(
_ node: Tree,
_ compositor: Compositor,
_ simpleTypes: [String: SimpleType],
) -> Particle {
var particles: [Particle] = []
for child in XSDNode.elementChildren(node) {
if let member = particle(child, simpleTypes) { particles.append(member) }
}
let (minimum, maximum) = XSDNode.occurrence(node)
return Particle(
minOccurs: minimum,
maxOccurs: maximum,
term: .group(Group(compositor: compositor, particles: particles)),
)
}
private static func particle(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> Particle? {
let (minimum, maximum) = XSDNode.occurrence(node)
switch XSDNode.localName(node) {
case "element":
let name = XSDNode.attribute(node, "name") ?? XSDNode.stripPrefix(XSDNode.attribute(node, "ref") ?? "")
let type = XSDNode.attribute(node, "name") != nil ? elementType(node, simpleTypes) : ElementType.complex(anyType)
return Particle(minOccurs: minimum, maxOccurs: maximum, term: .element(name: PureXML.Model.QualifiedName(name), type: type))
case "sequence":
return groupParticle(node, .sequence, simpleTypes)
case "choice":
return groupParticle(node, .choice, simpleTypes)
case "all":
return groupParticle(node, .all, simpleTypes)
case "any":
return Particle(minOccurs: minimum, maxOccurs: maximum, term: .wildcard(Wildcard()))
default:
return nil
}
}
}
}

View File

@ -0,0 +1,124 @@
@testable import PureXML
import Testing
@Suite("XSD list, union, groups, and references")
struct XSDCompositionTests {
private func validate(_ xsd: String, _ xml: String) throws -> [PureXML.Validation.Issue] {
try PureXML.Schema.Document(xsd).validate(xml)
}
@Test("A list simple type validates each whitespace-separated item")
func test_listType() throws {
let xsd = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:simpleType name="ints">
<xs:list itemType="xs:integer"/>
</xs:simpleType>
<xs:element name="nums" type="ints"/>
</xs:schema>
"""
#expect(try validate(xsd, "<nums>1 2 3</nums>").isEmpty)
#expect(try !validate(xsd, "<nums>1 x 3</nums>").isEmpty)
}
@Test("A restricted list bounds its item count")
func test_listLength() throws {
let xsd = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:simpleType name="pair">
<xs:restriction base="couples">
<xs:length value="2"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="couples">
<xs:list itemType="xs:string"/>
</xs:simpleType>
<xs:element name="p" type="pair"/>
</xs:schema>
"""
#expect(try validate(xsd, "<p>a b</p>").isEmpty)
#expect(try !validate(xsd, "<p>a b c</p>").isEmpty)
}
@Test("A union admits a value valid for any member type")
func test_unionType() throws {
let xsd = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:simpleType name="sizeOrAuto">
<xs:union memberTypes="xs:nonNegativeInteger">
<xs:simpleType>
<xs:restriction base="xs:string"><xs:enumeration value="auto"/></xs:restriction>
</xs:simpleType>
</xs:union>
</xs:simpleType>
<xs:element name="w" type="sizeOrAuto"/>
</xs:schema>
"""
#expect(try validate(xsd, "<w>10</w>").isEmpty)
#expect(try validate(xsd, "<w>auto</w>").isEmpty)
#expect(try !validate(xsd, "<w>-1</w>").isEmpty)
#expect(try !validate(xsd, "<w>tiny</w>").isEmpty)
}
@Test("An attribute group is expanded into the referencing type")
func test_attributeGroup() throws {
let xsd = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:attributeGroup name="dims">
<xs:attribute name="w" type="xs:integer" use="required"/>
<xs:attribute name="h" type="xs:integer" use="required"/>
</xs:attributeGroup>
<xs:element name="box">
<xs:complexType>
<xs:attributeGroup ref="dims"/>
</xs:complexType>
</xs:element>
</xs:schema>
"""
#expect(try validate(xsd, "<box w=\"3\" h=\"4\"/>").isEmpty)
#expect(try !validate(xsd, "<box w=\"3\"/>").isEmpty)
#expect(try !validate(xsd, "<box w=\"x\" h=\"4\"/>").isEmpty)
}
@Test("A named model group is expanded at its reference")
func test_groupReference() throws {
let xsd = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:group name="titleAndBody">
<xs:sequence>
<xs:element name="title" type="xs:string"/>
<xs:element name="body" type="xs:string"/>
</xs:sequence>
</xs:group>
<xs:element name="doc">
<xs:complexType>
<xs:sequence>
<xs:group ref="titleAndBody"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
"""
#expect(try validate(xsd, "<doc><title>A</title><body>B</body></doc>").isEmpty)
#expect(try !validate(xsd, "<doc><body>B</body></doc>").isEmpty)
}
@Test("An element reference resolves to the global declaration's type")
func test_elementReference() throws {
let xsd = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="sku" type="xs:nonNegativeInteger"/>
<xs:element name="order">
<xs:complexType>
<xs:sequence>
<xs:element ref="sku" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
"""
#expect(try validate(xsd, "<order><sku>1</sku><sku>2</sku></order>").isEmpty)
#expect(try !validate(xsd, "<order><sku>-1</sku></order>").isEmpty)
#expect(try !validate(xsd, "<order><other>1</other></order>").isEmpty)
}
}