Add Schema.Document compiling an xs:schema into global element declarations and a named-type table, and validate(_:) checking instance documents end to end. The parser supports global and local element declarations, named and inline simple and complex types, sequence/choice/all groups with occurrence, attribute uses, simple content (restriction/extension), the full facet set, and recursive types via named references resolved during validation. Extends ComplexType with a typeReference case and ComplexValidator with a type table so recursive schemas are representable. Closes #10
267 lines
12 KiB
Swift
267 lines
12 KiB
Swift
private typealias Tree = PureXML.Model.TreeNode
|
|
|
|
/// Tree-access helpers shared by the schema parser. File-scope and private.
|
|
private enum XSDNode {
|
|
static func localName(_ node: Tree) -> String? {
|
|
node.name?.localName
|
|
}
|
|
|
|
static func attribute(_ node: Tree, _ name: String) -> String? {
|
|
node.attributes.first { $0.name.localName == name }?.value
|
|
}
|
|
|
|
static func elementChildren(_ node: Tree) -> [Tree] {
|
|
node.children.filter { $0.kind == .element }
|
|
}
|
|
|
|
static func children(_ node: Tree, named name: String) -> [Tree] {
|
|
elementChildren(node).filter { localName($0) == name }
|
|
}
|
|
|
|
static func firstChild(_ node: Tree, named name: String) -> Tree? {
|
|
children(node, named: name).first
|
|
}
|
|
|
|
static func stripPrefix(_ qualified: String) -> String {
|
|
qualified.split(separator: ":").last.map(String.init) ?? qualified
|
|
}
|
|
|
|
static func occurrence(_ node: Tree) -> (min: Int, max: Int?) {
|
|
let minimum = attribute(node, "minOccurs").flatMap(Int.init) ?? 1
|
|
let maximumText = attribute(node, "maxOccurs")
|
|
let maximum: Int? = maximumText == "unbounded" ? nil : (maximumText.flatMap(Int.init) ?? 1)
|
|
return (minimum, maximum)
|
|
}
|
|
}
|
|
|
|
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.
|
|
enum XSDParser {
|
|
static func parse(
|
|
_ xsd: String,
|
|
) throws -> (elements: [String: ElementType], types: [String: ElementType]) {
|
|
let root = try PureXML.parseTree(xsd)
|
|
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 types: [String: ElementType] = 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))
|
|
}
|
|
}
|
|
var elements: [String: ElementType] = [:]
|
|
for node in XSDNode.children(schema, named: "element") {
|
|
if let name = XSDNode.attribute(node, "name") {
|
|
elements[name] = elementType(node, simpleTypes)
|
|
}
|
|
}
|
|
return (elements, types)
|
|
}
|
|
|
|
// MARK: Element and type references
|
|
|
|
private static func elementType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> ElementType {
|
|
if let typeName = XSDNode.attribute(node, "type") {
|
|
return typeReference(typeName)
|
|
}
|
|
if let inline = XSDNode.firstChild(node, named: "simpleType") {
|
|
return .simple(simpleType(inline, simpleTypes))
|
|
}
|
|
if let inline = XSDNode.firstChild(node, named: "complexType") {
|
|
return .complex(complexType(inline, simpleTypes))
|
|
}
|
|
return .complex(anyType)
|
|
}
|
|
|
|
private static func typeReference(_ typeName: String) -> ElementType {
|
|
let local = XSDNode.stripPrefix(typeName)
|
|
if let builtin = BuiltinType(rawValue: local) {
|
|
return .simple(SimpleType(base: builtin))
|
|
}
|
|
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,
|
|
content: .mixed(Particle(minOccurs: 0, maxOccurs: nil, term: .wildcard(Wildcard()))),
|
|
)
|
|
}
|
|
|
|
// MARK: Simple types and facets
|
|
|
|
private static func simpleType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> SimpleType {
|
|
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()
|
|
if let builtin = BuiltinType(rawValue: baseName) {
|
|
base = builtin
|
|
} else if let parent = simpleTypes[baseName] {
|
|
base = parent.base
|
|
facets = parent.facets
|
|
}
|
|
applyFacets(restriction, into: &facets)
|
|
return SimpleType(base: base, facets: facets)
|
|
}
|
|
|
|
private 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)
|
|
applyNumericFacet(XSDNode.localName(facet), value, into: &facets)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|