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.
119 lines
4.7 KiB
Swift
119 lines
4.7 KiB
Swift
extension PureXML.Pattern {
|
|
/// How a step connects to the step on its left (its parent in the pattern).
|
|
enum Gap: Equatable, Sendable {
|
|
/// The first step must be the document root element.
|
|
case root
|
|
/// The immediate parent (a single `/`).
|
|
case child
|
|
/// Any ancestor (a `//`, or a relative pattern's floating start).
|
|
case descendant
|
|
}
|
|
|
|
/// A name test in a pattern step.
|
|
enum NameMatch: Equatable, Sendable {
|
|
case wildcard
|
|
case name(String)
|
|
case prefixWildcard(String)
|
|
|
|
func matches(_ name: PureXML.Model.QualifiedName) -> Bool {
|
|
switch self {
|
|
case .wildcard:
|
|
return true
|
|
case let .prefixWildcard(prefix):
|
|
return name.prefix == prefix
|
|
case let .name(text):
|
|
if text.contains(":") {
|
|
return name.description == text
|
|
}
|
|
return name.description == text || name.localName == text
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One compiled pattern step.
|
|
struct PatternStep: Equatable, Sendable {
|
|
let gap: Gap
|
|
let test: NameMatch
|
|
}
|
|
}
|
|
|
|
public extension PureXML.Pattern {
|
|
/// A compiled streaming pattern (the libxml2 `pattern.h` XPath subset): element
|
|
/// names, `*`, `prefix:*`, `/`, `//`, an absolute leading `/`, and an optional
|
|
/// trailing attribute step. Predicates, `.`/`..`, and the general expression
|
|
/// language are out of the streamable subset and are rejected at compile time.
|
|
///
|
|
/// A pattern is matched against an element's ancestor-or-self path (root first),
|
|
/// which a streaming reader knows from its open-element stack, so no tree is
|
|
/// needed.
|
|
struct Matcher: Sendable {
|
|
let elementSteps: [PatternStep]
|
|
let attributeTest: NameMatch?
|
|
|
|
/// Compiles a pattern string.
|
|
public init(_ pattern: String) throws {
|
|
(elementSteps, attributeTest) = try PatternCompiler.compile(pattern)
|
|
}
|
|
|
|
/// Whether this pattern selects attributes rather than elements.
|
|
public var matchesAttributes: Bool {
|
|
attributeTest != nil
|
|
}
|
|
|
|
/// Whether the element at the end of `path` (root first) matches an
|
|
/// element pattern. Always false for an attribute pattern.
|
|
public func matchesElement(path: [PureXML.Model.QualifiedName]) -> Bool {
|
|
guard attributeTest == nil, !path.isEmpty else { return false }
|
|
return matchesElementPath(path)
|
|
}
|
|
|
|
/// The attributes of the element at the end of `path` that this pattern
|
|
/// selects. Empty for an element pattern.
|
|
public func matchingAttributes(
|
|
path: [PureXML.Model.QualifiedName],
|
|
attributes: [PureXML.Model.Attribute],
|
|
) -> [PureXML.Model.Attribute] {
|
|
guard let attributeTest, !path.isEmpty, matchesElementPath(path) else { return [] }
|
|
return attributes.filter { attributeTest.matches($0.name) }
|
|
}
|
|
|
|
/// Whether the element steps match `path` ending at the last element.
|
|
private func matchesElementPath(_ path: [PureXML.Model.QualifiedName]) -> Bool {
|
|
guard !elementSteps.isEmpty else { return true }
|
|
return matchUpTo(elementSteps.count - 1, path.count - 1, path)
|
|
}
|
|
|
|
/// Whether `elementSteps[0...stepIndex]` match the path ending at
|
|
/// `pathIndex`, with `elementSteps[stepIndex]` matching `path[pathIndex]`.
|
|
private func matchUpTo(_ stepIndex: Int, _ pathIndex: Int, _ path: [PureXML.Model.QualifiedName]) -> Bool {
|
|
guard pathIndex >= 0 else { return false }
|
|
let step = elementSteps[stepIndex]
|
|
guard step.test.matches(path[pathIndex]) else { return false }
|
|
if stepIndex == 0 {
|
|
return step.gap == .root ? pathIndex == 0 : true
|
|
}
|
|
switch step.gap {
|
|
case .child:
|
|
return matchUpTo(stepIndex - 1, pathIndex - 1, path)
|
|
case .descendant:
|
|
return ancestorMatches(stepIndex - 1, below: pathIndex, path)
|
|
case .root:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Whether `elementSteps[0...stepIndex]` match some ancestor strictly above
|
|
/// `pathIndex` (the `//` gap allows any number of intervening ancestors).
|
|
private func ancestorMatches(_ stepIndex: Int, below pathIndex: Int, _ path: [PureXML.Model.QualifiedName]) -> Bool {
|
|
var candidate = pathIndex - 1
|
|
while candidate >= 0 {
|
|
if matchUpTo(stepIndex, candidate, path) {
|
|
return true
|
|
}
|
|
candidate -= 1
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
}
|