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
100 lines
4.5 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|
|
}
|