PureXML/Sources/Schema/XSDSimpleTypeOrder.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

72 lines
3.3 KiB
Swift

private typealias XSDNode = PureXML.Schema.XSDNode
extension PureXML.Schema.XSDParser {
/// The named `<xs:simpleType>` nodes keyed by name (first declaration wins, matching
/// the resolution loop, which overwrites a later same-name node's slot last).
static func simpleTypeNodesByName(_ named: [PureXML.Model.TreeNode]) -> [String: PureXML.Model.TreeNode] {
var byName: [String: PureXML.Model.TreeNode] = [:]
for node in named {
if let name = XSDNode.attribute(node, "name"), byName[name] == nil {
byName[name] = node
}
}
return byName
}
/// The named simple types in dependency order: each type after every named type it
/// derives from (its `restriction` base, `list` item, or `union` members, including
/// those reached through an inline anonymous base). Resolving in this order lets each
/// type be compiled ONCE with its base already in `context.simpleTypes`, where the
/// former fixpoint re-resolved every type N times (super-linear on a long restriction
/// chain). An iterative post-order DFS, so it does not recurse on the chain depth; a
/// name already on the current path is not re-entered, so a circular reference
/// (illegal, rejected elsewhere) terminates and its types still get an order.
static func simpleTypeResolutionOrder(
_ named: [PureXML.Model.TreeNode],
nodeByName: [String: PureXML.Model.TreeNode],
) -> [String] {
var order: [String] = []
var visited: Set<String> = []
var onPath: Set<String> = []
var work: [(name: String, exit: Bool)] = named
.compactMap { XSDNode.attribute($0, "name") }
.reversed()
.map { ($0, false) }
while let (name, exit) = work.popLast() {
if exit {
onPath.remove(name)
order.append(name)
continue
}
if visited.contains(name) {
continue
}
visited.insert(name)
onPath.insert(name)
work.append((name, true))
guard let node = nodeByName[name] else { continue }
for dependency in referencedSimpleTypeNames(in: node) {
guard nodeByName[dependency] != nil, !visited.contains(dependency), !onPath.contains(dependency) else { continue }
work.append((dependency, false))
}
}
return order
}
/// The type names a `<xs:simpleType>` node references through `base`, `itemType`, or
/// `memberTypes` (the last a space-separated list), anywhere in its subtree, by an
/// iterative walk. The caller keeps only those that name another type in the set.
private static func referencedSimpleTypeNames(in node: PureXML.Model.TreeNode) -> [String] {
var names: [String] = []
var stack = [node]
while let current = stack.popLast() {
for attribute in ["base", "itemType", "memberTypes"] {
guard let value = XSDNode.attribute(current, attribute) else { continue }
names += value.split(separator: " ").map { XSDNode.stripPrefix(String($0)) }
}
stack.append(contentsOf: XSDNode.elementChildren(current))
}
return names
}
}