PureXML/Sources/Schema/XSDSimpleParser.swift
Mihaela Mihaljevic 4a966260f3 feat(xsd): add include/import/redefine, substitution groups, and xsi:type
Compose schemas across documents and resolve types at the instance:

- xs:include, xs:import, and xs:redefine pull a transitive closure of schema
  documents through an injected loader (resolved by schemaLocation), base
  schemas first so the including schema wins; a redefine's nested definitions
  override the schema it includes.
- substitution groups: an xs:element ref to a head also admits every element
  that substitutes for it, expanded at parse time into a choice over the head
  and its transitive members.
- xsi:type overrides an element's declared type with the named type at the
  instance; XML Schema instance attributes are exempt from attribute checks.

The schema parser splits across XSDContext (shared tree helpers, document
collection, parsing context), XSDSimpleParser (simple types and facets), and
XSDParser (elements, complex types, model groups) to keep each within bounds.

Closes #66
2026-06-08 08:46:24 +02:00

100 lines
4.5 KiB
Swift

extension PureXML.Schema {
/// Parses XSD simple types: atomic restrictions with the full facet set, and
/// the `list` and `union` varieties. Kept beside ``XSDParser``, sharing its
/// module-scope tree helpers and parsing context.
enum XSDSimpleParser {
static func simpleType(_ node: XSDTree, _ context: XSDContext) -> 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 = context.simpleTypes[baseName] {
base = parent.base
facets = parent.facets
variety = parent.variety
}
applyFacets(restriction, into: &facets)
return SimpleType(base: base, facets: facets, variety: variety)
}
static func simpleTypeReference(_ typeName: String, _ context: XSDContext) -> SimpleType {
let local = XSDNode.stripPrefix(typeName)
if let builtin = BuiltinType(rawValue: local) { return SimpleType(base: builtin) }
return context.simpleTypes[local] ?? SimpleType(base: .string)
}
static func applyFacets(_ restriction: XSDTree, into facets: inout Facets) {
for facet in XSDNode.elementChildren(restriction) {
let value = XSDNode.attribute(facet, "value")
applyStringFacet(XSDNode.localName(facet), value, into: &facets)
applyNumericFacet(XSDNode.localName(facet), value, into: &facets)
}
}
private static func listType(_ node: XSDTree, _ context: XSDContext) -> 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: XSDTree, _ context: XSDContext) -> 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) }
case "enumeration": if let value { facets.enumeration = (facets.enumeration ?? []) + [value] }
case "minInclusive": facets.minInclusive = value
case "maxInclusive": facets.maxInclusive = value
case "minExclusive": facets.minExclusive = value
case "maxExclusive": facets.maxExclusive = value
case "whiteSpace": facets.whiteSpace = whiteSpace(value)
default: break
}
}
private static func applyNumericFacet(_ name: String?, _ value: String?, into facets: inout Facets) {
let number = value.flatMap(Int.init)
switch name {
case "length": facets.length = number
case "minLength": facets.minLength = number
case "maxLength": facets.maxLength = number
case "totalDigits": facets.totalDigits = number
case "fractionDigits": facets.fractionDigits = number
default: break
}
}
private static func whiteSpace(_ value: String?) -> WhiteSpace? {
switch value {
case "preserve": .preserve
case "replace": .replace
case "collapse": .collapse
default: nil
}
}
}
}