PureXML/Sources/XPath/DocumentOrder.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

104 lines
5.0 KiB
Swift

extension PureXML.XPath.AxisNavigation {
/// A document-order sort key. Comparing two nodes is O(1): the document each
/// belongs to (ranked by first appearance), the pre-order index of the node's
/// owning tree node, a band placing an element before its namespace nodes
/// before its attribute nodes before its children, and a tie-breaking
/// sub-index. This replaces the former per-node root path, an array as long as
/// the node's depth, which made ordering a deep document quadratic.
private struct OrderKey: Comparable {
var rootRank = 0
var index = 0
var band = 0
var sub = 0
static func < (lhs: OrderKey, rhs: OrderKey) -> Bool {
if lhs.rootRank != rhs.rootRank {
return lhs.rootRank < rhs.rootRank
}
if lhs.index != rhs.index {
return lhs.index < rhs.index
}
if lhs.band != rhs.band {
return lhs.band < rhs.band
}
return lhs.sub < rhs.sub
}
}
/// Sorts `nodes` into XPath document order using a per-root pre-order index
/// (built once and cached) rather than a per-node root path, so the sort is
/// O(n log n + document) instead of O(n log n x depth).
///
/// A node-set spanning several documents (an XSLT `document()` result) is
/// grouped by root, ranked by first appearance; only a node outside the first
/// node's document pays an O(depth) root walk, so the common single-document
/// case never walks a node's ancestors.
static func sortByDocumentOrder(_ nodes: [PureXML.XPath.Node], cache: PureXML.XPath.DocumentNavigationCache?) -> [PureXML.XPath.Node] {
guard nodes.count > 1, let primaryRoot = rootTree(of: nodes[0]) else { return nodes }
return decorate(nodes, primaryRoot: primaryRoot, cache: cache)
.sorted { $0.key < $1.key }
.map(\.node)
}
/// The single node first in document order, by the same keying as
/// ``sortByDocumentOrder(_:cache:)`` but in O(n) rather than a full sort.
static func firstByDocumentOrder(_ nodes: [PureXML.XPath.Node], cache: PureXML.XPath.DocumentNavigationCache?) -> PureXML.XPath.Node? {
guard nodes.count > 1, let primaryRoot = rootTree(of: nodes[0]) else { return nodes.first }
return decorate(nodes, primaryRoot: primaryRoot, cache: cache)
.min { $0.key < $1.key }?
.node
}
/// Pairs each node with its document-order key. The primary document's index
/// is built once; a foreign node falls back to its own document's index,
/// ranked after the primary by first appearance.
private static func decorate(
_ nodes: [PureXML.XPath.Node],
primaryRoot: PureXML.Model.TreeNode,
cache: PureXML.XPath.DocumentNavigationCache?,
) -> [(node: PureXML.XPath.Node, key: OrderKey)] {
let primaryID = ObjectIdentifier(primaryRoot)
var indexByRoot: [ObjectIdentifier: [PureXML.XPath.Node: Int]] = [primaryID: orderedDocument(rootedAt: primaryRoot, cache: cache).index]
var rootRank: [ObjectIdentifier: Int] = [primaryID: 0]
return nodes.map { node in
if var key = compactKey(node, in: indexByRoot[primaryID] ?? [:]) {
key.rootRank = 0
return (node, key)
}
guard let root = rootTree(of: node) else { return (node, OrderKey(rootRank: .max)) }
let rootID = ObjectIdentifier(root)
let rank: Int
if let existing = rootRank[rootID] {
rank = existing
} else {
rank = rootRank.count
rootRank[rootID] = rank
indexByRoot[rootID] = orderedDocument(rootedAt: root, cache: cache).index
}
var key = compactKey(node, in: indexByRoot[rootID] ?? [:]) ?? OrderKey()
key.rootRank = rank
return (node, key)
}
}
/// `node`'s order key within `index` (a pre-order numbering of one document's
/// tree nodes), or nil when `node` belongs to a different document. The band
/// orders an element before its namespace nodes before its attribute nodes
/// before its children (whose own index is greater), mirroring the bands of
/// the former path-key form.
private static func compactKey(_ node: PureXML.XPath.Node, in index: [PureXML.XPath.Node: Int]) -> OrderKey? {
switch node {
case .tree:
guard let position = index[node] else { return nil }
return OrderKey(index: position, band: 0)
case let .attribute(owner, attribute):
guard let position = index[.tree(owner)] else { return nil }
let attributeIndex = owner.attributes.firstIndex { $0.name == attribute.name } ?? 0
return OrderKey(index: position, band: 2, sub: attributeIndex)
case let .namespace(owner, prefix, _):
guard let position = index[.tree(owner)] else { return nil }
return OrderKey(index: position, band: 1, sub: prefix.hashValue)
}
}
}