PureXML/Sources/Model/Node.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

134 lines
5.7 KiB
Swift

public extension PureXML.Model {
/// A node in an XML tree. The model preserves document order and the
/// distinction between text, CDATA, comments, and processing instructions
/// so that emitting can round-trip a parsed document.
///
/// Not `indirect`: an element holds its children behind a reference
/// (``ElementStorage``), so a `Node` is a fixed-size value and the tree's
/// depth is not part of any value's layout. Equality, hashing, and release
/// all walk the tree iteratively, so an arbitrarily deep document never
/// overflows the call stack.
enum Node: Sendable {
/// The root of a parsed document, holding its prolog and root element.
case document([Node])
/// An element with a name, attributes, and children.
case element(Element)
/// Character data between markup.
case text(String)
/// A `<![CDATA[ ... ]]>` section. Stored as its raw inner text.
case cdata(String)
/// A `<!-- ... -->` comment. Stored as its raw inner text.
case comment(String)
/// A `<?target data?>` processing instruction.
case processingInstruction(target: String, data: String)
/// The wrapped ``Element`` when this node is an element.
public var element: Element? {
guard case let .element(element) = self else { return nil }
return element
}
}
}
extension PureXML.Model.Node: Equatable, Hashable {
public static func == (lhs: Self, rhs: Self) -> Bool {
treesEqual(lhs, rhs)
}
/// Whether two node trees are equal, compared iteratively through an explicit
/// pair stack so a deeply-nested tree does not recurse one frame per level.
static func treesEqual(_ lhs: Self, _ rhs: Self) -> Bool {
var stack: [(Self, Self)] = [(lhs, rhs)]
while let (left, right) = stack.popLast() {
switch (left, right) {
case let (.element(leftElement), .element(rightElement)):
guard leftElement.name == rightElement.name,
leftElement.attributes == rightElement.attributes,
leftElement.children.count == rightElement.children.count
else { return false }
for pair in zip(leftElement.children, rightElement.children) {
stack.append(pair)
}
case let (.document(leftChildren), .document(rightChildren)):
guard leftChildren.count == rightChildren.count else { return false }
for pair in zip(leftChildren, rightChildren) {
stack.append(pair)
}
default:
if !leavesEqual(left, right) {
return false
}
}
}
return true
}
/// Equality for the non-branch node kinds (and a kind mismatch); the branch
/// kinds are walked iteratively by ``treesEqual(_:_:)``.
private static func leavesEqual(_ left: Self, _ right: Self) -> Bool {
switch (left, right) {
case let (.text(leftValue), .text(rightValue)): leftValue == rightValue
case let (.cdata(leftValue), .cdata(rightValue)): leftValue == rightValue
case let (.comment(leftValue), .comment(rightValue)): leftValue == rightValue
case let (.processingInstruction(leftTarget, leftData), .processingInstruction(rightTarget, rightData)):
leftTarget == rightTarget && leftData == rightData
default: false
}
}
/// Hashes the whole subtree iteratively (pre-order), consistent with
/// ``treesEqual(_:_:)``: equal trees feed the hasher the same sequence.
public func hash(into hasher: inout Hasher) {
var stack: [Self] = [self]
while let node = stack.popLast() {
switch node {
case let .element(element):
hasher.combine(0)
hasher.combine(element.name)
hasher.combine(element.attributes)
hasher.combine(element.children.count)
for child in element.children {
stack.append(child)
}
case let .document(children):
hasher.combine(1)
hasher.combine(children.count)
for child in children {
stack.append(child)
}
case let .text(value):
hasher.combine(2)
hasher.combine(value)
case let .cdata(value):
hasher.combine(3)
hasher.combine(value)
case let .comment(value):
hasher.combine(4)
hasher.combine(value)
case let .processingInstruction(target, data):
hasher.combine(5)
hasher.combine(target)
hasher.combine(data)
}
}
}
}
extension PureXML.Model.Node {
/// Used by ``ElementStorage`` teardown: if this is an element whose backing
/// storage this node solely owns, move its children into `pending` and clear
/// them so the node releases flat; otherwise leave it untouched. Setting
/// `self` aside before the uniqueness check drops this node's own hold on the
/// storage, so the check counts only references held elsewhere (shared
/// copy-on-write siblings), which must keep their subtree.
mutating func drainUniquelyOwnedChildren(into pending: inout [Self]) {
guard case var .element(element) = self else { return }
self = .text("")
if isKnownUniquelyReferenced(&element.storage), !element.storage.children.isEmpty {
pending.append(contentsOf: element.storage.children)
element.storage.children = []
}
self = .element(element)
}
}