PureXML/Sources/Canonical/CanonicalNodeSubset.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

79 lines
3.5 KiB
Swift

extension PureXML.Canonical.Canonicalizer {
/// The apex element with its inherited `xml:*` attributes added, and (for
/// inclusive mode) the namespaces in scope from omitted ancestors merged in
/// as declarations so the apex renders its full namespace context.
static func augmentedApex(
_ apex: PureXML.Model.Element,
inheritedXML: [PureXML.Model.Attribute],
mergingNamespaces ancestorNamespaces: [String: String]?,
stripApexBase: Bool,
) -> PureXML.Model.Element {
// Under 1.1, the apex's own xml:base is folded into the merged base in
// inheritedXML, so drop the original to avoid emitting it twice.
let own = stripApexBase
? apex.attributes.filter { !($0.name.prefix == "xml" && $0.name.localName == "base") }
: apex.attributes
var attributes = own + inheritedXML
if let ancestorNamespaces {
let declared = Set(apex.attributes.map(\.name.description))
for (prefix, uri) in ancestorNamespaces {
let name = prefix.isEmpty ? "xmlns" : "xmlns:\(prefix)"
if !declared.contains(name) {
attributes.append(PureXML.Model.Attribute(name, uri))
}
}
}
return PureXML.Model.Element(name: apex.name, attributes: attributes, children: apex.children)
}
/// The namespace declarations in scope from a node's omitted ancestors,
/// nearest ancestor winning (so an inner redeclaration is preserved).
static func inScopeNamespaces(above node: PureXML.Model.TreeNode) -> [String: String] {
var result: [String: String] = [:]
var current = node.parent
while let ancestor = current {
for attribute in ancestor.attributes {
guard let prefix = declaredPrefix(of: attribute.name), result[prefix] == nil else { continue }
result[prefix] = attribute.value
}
current = ancestor.parent
}
return result
}
/// The `xml:*` attributes in scope from a node's omitted ancestors that the
/// apex does not already set, nearest ancestor winning.
static func inheritedXMLAttributes(above node: PureXML.Model.TreeNode, apex: PureXML.Model.Element, mergeBase: Bool) -> [PureXML.Model.Attribute] {
let present = Set(apex.attributes.map(\.name.description))
if mergeBase {
return inherited11XMLAttributes(above: node, apex: apex, present: present)
}
// Canonical XML 1.0: the nearest of every in-scope xml:* attribute the
// apex does not already set.
var seen: Set<String> = []
var result: [PureXML.Model.Attribute] = []
var current = node.parent
while let ancestor = current {
for attribute in ancestor.attributes where attribute.name.prefix == "xml" {
let key = attribute.name.description
guard !present.contains(key), seen.insert(key).inserted else { continue }
result.append(attribute)
}
current = ancestor.parent
}
return result
}
/// The prefix a namespace declaration binds (empty for the default
/// namespace), or nil when the attribute is not a namespace declaration.
static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? {
if name.prefix == nil, name.localName == "xmlns" {
return ""
}
if name.prefix == "xmlns" {
return name.localName
}
return nil
}
}