PureXML/Sources/Schema/SchemaSimpleTypeValidity.swift
Mihaela Mihaljevic 380160cc2b refactor(validation): decompose XSD instance validation into composable rules
The two monolithic XSD content/identity rules only satisfied the OpenAPIKit
validation idiom at the wrong granularity: no per-constraint removability,
no isolable failing/near-miss tests. Split them into 32 named rules, each
finding tagged at its origin and cached per XSDContext so the walk still
runs once per document (the #101 decision against duplicating XSD's
recursive type resolution still holds). Wired the streaming path's ID/IDREF
and identity checks through the same framework instead of calling the
validators imperatively, and added the unwrapAndLookup combinator the rule
already named as required.

Also includes the mandated repo-wide swiftformat/swiftlint pass.
2026-07-12 14:12:17 +02:00

122 lines
5.5 KiB
Swift

extension PureXML.Schema.XSDParser {
/// Enforces variety constraints on simpleTypes (XSD 1.0 Datatypes §3.3.1 / §3.4.1):
/// the itemType of a list must be atomic or union (where all member types are atomic).
/// That is, a list's itemType cannot be a list or a union containing a list, and
/// cannot resolve to the simple ur-type 'anySimpleType'.
static func simpleTypeVarietyFindings(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
let targetNamespace = context.targetNamespace
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
func check(_ node: XSDTree) {
let local = PureXML.Schema.XSDNode.localName(node)
if local == "appinfo" || local == "documentation" {
return
}
if node.name?.namespaceURI == xsdNamespace, local == "list" {
let item: PureXML.Schema.SimpleType = if let itemType = PureXML.Schema.XSDNode.attribute(node, "itemType") {
PureXML.Schema.XSDSimpleParser.simpleTypeReference(itemType, context)
} else if let inline = PureXML.Schema.XSDNode.elementChildren(node).first(where: { PureXML.Schema.XSDNode.localName($0) == "simpleType" }) {
PureXML.Schema.XSDSimpleParser.simpleType(inline, context)
} else {
PureXML.Schema.SimpleType(base: .string)
}
var isForeign = false
if let itemType = PureXML.Schema.XSDNode.attribute(node, "itemType") {
let bindings = inScopeNamespaceBindings(of: node)
let prefix = PureXML.Schema.XSDNode.prefix(itemType)
let uri = prefix.flatMap { bindings[$0] } ?? bindings[""]
if uri != targetNamespace, uri != xsdNamespace, uri != nil, uri != "" {
isForeign = true
}
}
if !isForeign, isListOrUnionContainingList(item) {
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "the item type of a list must be atomic or union of atomic, not a list or union containing list",
node: node,
))
}
// src-simple-type.2: a list has its item type from EITHER the
// itemType attribute OR an inline simpleType child, never both and
// never neither.
if hasItemTypeAttribute(node) == hasSimpleTypeChild(node) {
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "a list must have either an 'itemType' attribute or an inline simpleType child, but not both",
node: node,
))
}
}
// src-simple-type.3: a union draws its members from a memberTypes
// attribute, inline simpleType children, or both, so it must declare at
// least one of them (an empty union has no member type definitions).
if isEmptyUnion(node, local) {
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "a union must declare at least one member type, through 'memberTypes' or an inline simpleType child",
node: node,
))
}
for child in PureXML.Schema.XSDNode.elementChildren(node) {
check(child)
}
}
check(schema)
return findings
}
/// A `union` element in the schema namespace that declares no member types at
/// all: neither a `memberTypes` attribute nor an inline `simpleType` child.
private static func isEmptyUnion(_ node: XSDTree, _ local: String?) -> Bool {
node.name?.namespaceURI == xsdNamespace && local == "union"
&& PureXML.Schema.XSDNode.attribute(node, "memberTypes") == nil
&& !hasSimpleTypeChild(node)
}
private static func hasItemTypeAttribute(_ node: XSDTree) -> Bool {
PureXML.Schema.XSDNode.attribute(node, "itemType") != nil
}
private static func hasSimpleTypeChild(_ node: XSDTree) -> Bool {
PureXML.Schema.XSDNode.elementChildren(node).contains { PureXML.Schema.XSDNode.localName($0) == "simpleType" }
}
private static func inScopeNamespaceBindings(of node: XSDTree) -> [String: String] {
var bindings: [String: String] = [:]
var current: XSDTree? = node
while let element = current {
for attribute in element.attributes {
if attribute.name.prefix == "xmlns" {
let prefix = attribute.name.localName
if bindings[prefix] == nil {
bindings[prefix] = attribute.value
}
} else if attribute.name.prefix == nil, attribute.name.localName == "xmlns" {
if bindings[""] == nil {
bindings[""] = attribute.value
}
}
}
current = element.parent
}
return bindings
}
private static func isListOrUnionContainingList(_ type: PureXML.Schema.SimpleType) -> Bool {
if type.isAnySimpleType {
return true
}
switch type.variety {
case .list:
return true
case let .union(members):
return members.contains { isListOrUnionContainingList($0) }
case .atomic:
return false
}
}
}