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.
231 lines
12 KiB
Swift
231 lines
12 KiB
Swift
/// A mutable bag of rebuilt nodes a branch's children gather into, so the
|
|
/// canonical prefix rewrite's deferred build step assembles the parent once they
|
|
/// are done.
|
|
private final class CanonicalRewriteAccumulator {
|
|
var nodes: [PureXML.Model.Node] = []
|
|
}
|
|
|
|
/// One unit of canonical prefix-rewrite work: visit a source node (appending its
|
|
/// rebuilt result to `target`), or, after a branch's children are gathered, build
|
|
/// the element or document. File-scoped so the work stack is not a third level of
|
|
/// type nesting.
|
|
private enum CanonicalRewriteWork {
|
|
case visit(PureXML.Model.Node, inScope: [String: String], isRoot: Bool, target: CanonicalRewriteAccumulator)
|
|
case buildElement(PureXML.Model.Element, childInScope: [String: String], isRoot: Bool, gathered: CanonicalRewriteAccumulator, target: CanonicalRewriteAccumulator)
|
|
case buildDocument(CanonicalRewriteAccumulator, CanonicalRewriteAccumulator)
|
|
}
|
|
|
|
extension PureXML.Canonical {
|
|
/// The Canonical XML 2.0 sequential prefix rewrite. Implemented as a pre-pass
|
|
/// that rebuilds the tree using canonical prefixes (`n0`, `n1`, ... in
|
|
/// document order of first use), drops the original namespace declarations,
|
|
/// and hoists one canonical declaration per namespace onto the document
|
|
/// element. The rewritten tree is then rendered exclusive-style so each
|
|
/// declaration appears at the first element that uses it, with no default
|
|
/// namespace (every name carries a prefix). The reserved `xml` prefix is left
|
|
/// untouched.
|
|
enum PrefixRewriter {
|
|
private static let xmlNamespace = "http://www.w3.org/XML/1998/namespace"
|
|
|
|
/// Rewrites a node tree's prefixes (and any QName-valued labels) to their
|
|
/// canonical sequential form.
|
|
static func rewrite(_ node: PureXML.Model.Node, labels: [QNameAwareLabel]) -> PureXML.Model.Node {
|
|
var assignment: [String: String] = [:]
|
|
var order = 0
|
|
assign(node, inScope: [:], labels: labels, &assignment, &order)
|
|
return transform(node, assignment: assignment, labels: labels)
|
|
}
|
|
|
|
// MARK: First pass: assign a canonical prefix per visibly-used namespace
|
|
|
|
private static func assign(
|
|
_ root: PureXML.Model.Node,
|
|
inScope: [String: String],
|
|
labels: [QNameAwareLabel],
|
|
_ assignment: inout [String: String],
|
|
_ order: inout Int,
|
|
) {
|
|
// Iterative pre-order walk so a deeply-nested document does not overflow
|
|
// the stack; children push reversed so namespaces are recorded in
|
|
// document order, the order the canonical prefixes n0, n1, ... assign in.
|
|
var stack: [(node: PureXML.Model.Node, inScope: [String: String])] = [(root, inScope)]
|
|
while let (node, inScope) = stack.popLast() {
|
|
switch node {
|
|
case let .document(children):
|
|
for child in children.reversed() {
|
|
stack.append((child, inScope))
|
|
}
|
|
case let .element(element):
|
|
var childInScope = inScope
|
|
for (prefix, uri) in declarations(element) {
|
|
childInScope[prefix] = uri
|
|
}
|
|
record(element.name.namespaceURI, &assignment, &order)
|
|
for attribute in element.attributes where attribute.name.prefix != nil {
|
|
record(attribute.name.namespaceURI, &assignment, &order)
|
|
}
|
|
for attribute in element.attributes where isQNameAttribute(attribute.name, labels) {
|
|
record(qnameNamespace(attribute.value, inScope: childInScope), &assignment, &order)
|
|
}
|
|
record(qnameNamespace(element.text(matching: labels), inScope: childInScope), &assignment, &order)
|
|
for child in element.children.reversed() {
|
|
stack.append((child, childInScope))
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The namespace URI a QName value refers to through its prefix, or nil.
|
|
private static func qnameNamespace(_ value: String?, inScope: [String: String]) -> String? {
|
|
guard let value else { return nil }
|
|
let parts = value.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
|
|
return inScope[parts.count == 2 ? String(parts[0]) : ""]
|
|
}
|
|
|
|
private static func isQNameAttribute(_ name: PureXML.Model.QualifiedName, _ labels: [QNameAwareLabel]) -> Bool {
|
|
labels.contains { !$0.isElement && $0.localName == name.localName && $0.namespaceURI == (name.namespaceURI ?? "") }
|
|
}
|
|
|
|
private static func record(_ uri: String?, _ assignment: inout [String: String], _ order: inout Int) {
|
|
guard let uri, uri != xmlNamespace, assignment[uri] == nil else { return }
|
|
assignment[uri] = "n\(order)"
|
|
order += 1
|
|
}
|
|
|
|
// MARK: Second pass: rebuild with canonical prefixes
|
|
|
|
/// Rebuilds the tree with canonical prefixes without recursing on its depth.
|
|
/// An explicit work stack threads the in-scope declarations and the root
|
|
/// flag down per element, while each branch gathers its rebuilt children
|
|
/// into a reference accumulator that a deferred build step assembles, so a
|
|
/// deeply-nested document does not overflow the stack.
|
|
private static func transform(
|
|
_ root: PureXML.Model.Node,
|
|
assignment: [String: String],
|
|
labels: [QNameAwareLabel],
|
|
) -> PureXML.Model.Node {
|
|
let result = CanonicalRewriteAccumulator()
|
|
var stack: [CanonicalRewriteWork] = [.visit(root, inScope: [:], isRoot: true, target: result)]
|
|
while let work = stack.popLast() {
|
|
switch work {
|
|
case let .visit(node, inScope, isRoot, target):
|
|
visitForRewrite(node, inScope: inScope, isRoot: isRoot, target: target, stack: &stack)
|
|
case let .buildElement(element, childInScope, isRoot, gathered, target):
|
|
var attributes = isRoot ? canonicalDeclarations(assignment) : []
|
|
for attribute in element.attributes where declaredPrefix(of: attribute.name) == nil {
|
|
attributes.append(rewrite(attribute, inScope: childInScope, assignment: assignment, labels: labels))
|
|
}
|
|
// A QName-labelled element's single text child is rewritten to a
|
|
// canonical-prefixed QName, replacing its rebuilt children.
|
|
let qualified = qname(element.text(matching: labels), inScope: childInScope, assignment: assignment)
|
|
target.nodes.append(.element(PureXML.Model.Element(
|
|
name: rename(element.name, assignment: assignment),
|
|
attributes: attributes,
|
|
children: qualified.map { [PureXML.Model.Node.text($0)] } ?? gathered.nodes,
|
|
)))
|
|
case let .buildDocument(gathered, target):
|
|
target.nodes.append(.document(gathered.nodes))
|
|
}
|
|
}
|
|
return result.nodes.first ?? root
|
|
}
|
|
|
|
/// Pushes a node's rebuild work: a document or element schedules its build
|
|
/// step then its children (with the in-scope declarations extended), a leaf
|
|
/// is appended to `target` unchanged.
|
|
private static func visitForRewrite(
|
|
_ node: PureXML.Model.Node,
|
|
inScope: [String: String],
|
|
isRoot: Bool,
|
|
target: CanonicalRewriteAccumulator,
|
|
stack: inout [CanonicalRewriteWork],
|
|
) {
|
|
switch node {
|
|
case let .document(children):
|
|
let gathered = CanonicalRewriteAccumulator()
|
|
stack.append(.buildDocument(gathered, target))
|
|
for child in children.reversed() {
|
|
stack.append(.visit(child, inScope: inScope, isRoot: isRoot, target: gathered))
|
|
}
|
|
case let .element(element):
|
|
var childInScope = inScope
|
|
for (prefix, uri) in declarations(element) {
|
|
childInScope[prefix] = uri
|
|
}
|
|
let gathered = CanonicalRewriteAccumulator()
|
|
stack.append(.buildElement(element, childInScope: childInScope, isRoot: isRoot, gathered: gathered, target: target))
|
|
for child in element.children.reversed() {
|
|
stack.append(.visit(child, inScope: childInScope, isRoot: false, target: gathered))
|
|
}
|
|
default:
|
|
target.nodes.append(node)
|
|
}
|
|
}
|
|
|
|
/// One `xmlns:nK="uri"` declaration per assigned namespace, hoisted to the
|
|
/// document element so every canonical prefix is in scope from the top.
|
|
private static func canonicalDeclarations(_ assignment: [String: String]) -> [PureXML.Model.Attribute] {
|
|
assignment.sorted { $0.value < $1.value }.map { uri, prefix in
|
|
PureXML.Model.Attribute("xmlns:\(prefix)", uri)
|
|
}
|
|
}
|
|
|
|
private static func rewrite(
|
|
_ attribute: PureXML.Model.Attribute,
|
|
inScope: [String: String],
|
|
assignment: [String: String],
|
|
labels: [QNameAwareLabel],
|
|
) -> PureXML.Model.Attribute {
|
|
let name = rename(attribute.name, assignment: assignment)
|
|
guard isQNameAttribute(attribute.name, labels), let value = qname(attribute.value, inScope: inScope, assignment: assignment) else {
|
|
return PureXML.Model.Attribute(name: name, value: attribute.value)
|
|
}
|
|
return PureXML.Model.Attribute(name: name, value: value)
|
|
}
|
|
|
|
/// A qualified name with its prefix replaced by the canonical one. Returns
|
|
/// nil when there is nothing to rewrite (no bound prefix for the value).
|
|
private static func qname(_ value: String?, inScope: [String: String], assignment: [String: String]) -> String? {
|
|
guard let value else { return nil }
|
|
let parts = value.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
|
|
let prefix = parts.count == 2 ? String(parts[0]) : ""
|
|
let local = parts.count == 2 ? String(parts[1]) : value
|
|
guard let uri = inScope[prefix], let canonical = assignment[uri] else { return nil }
|
|
return "\(canonical):\(local)"
|
|
}
|
|
|
|
private static func rename(_ name: PureXML.Model.QualifiedName, assignment: [String: String]) -> PureXML.Model.QualifiedName {
|
|
guard let uri = name.namespaceURI, uri != xmlNamespace, let canonical = assignment[uri] else { return name }
|
|
return PureXML.Model.QualifiedName(prefix: canonical, localName: name.localName, namespaceURI: uri)
|
|
}
|
|
|
|
private static func declarations(_ element: PureXML.Model.Element) -> [(String, String)] {
|
|
element.attributes.compactMap { attribute in
|
|
declaredPrefix(of: attribute.name).map { ($0, attribute.value) }
|
|
}
|
|
}
|
|
|
|
private 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
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PureXML.Model.Element {
|
|
/// The text content of this element when it is labelled QName-aware as an
|
|
/// element (a single text child holding a QName), or nil otherwise.
|
|
func text(matching labels: [PureXML.Canonical.QNameAwareLabel]) -> String? {
|
|
guard labels.contains(where: { $0.isElement && $0.localName == name.localName && $0.namespaceURI == (name.namespaceURI ?? "") }) else { return nil }
|
|
guard children.count == 1, case let .text(value) = children[0] else { return nil }
|
|
return value
|
|
}
|
|
}
|