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.
133 lines
6.3 KiB
Swift
133 lines
6.3 KiB
Swift
extension PureXML.XPath {
|
|
/// The EXSLT extension functions (http://exslt.org/...), dispatched by the
|
|
/// namespace a function's prefix resolves to: the `math`, `sets`, and the
|
|
/// `common` modules. Registered through ``FunctionTable`` so they are
|
|
/// available wherever a prefix is bound to an EXSLT namespace.
|
|
enum EXSLT {
|
|
static let common = "http://exslt.org/common"
|
|
static let math = "http://exslt.org/math"
|
|
static let sets = "http://exslt.org/sets"
|
|
|
|
/// The implementation for an EXSLT `local` name in namespace `uri`, or nil.
|
|
static func implementation(uri: String, local: String) -> FunctionImplementation? {
|
|
switch uri {
|
|
case math: mathFunction(local)
|
|
case sets: setsFunction(local)
|
|
case common: commonFunction(local)
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
// MARK: common
|
|
|
|
private static func commonFunction(_ local: String) -> FunctionImplementation? {
|
|
guard local == "object-type" else { return nil }
|
|
return { arguments, _ in
|
|
switch arguments.first {
|
|
case .nodeSet: .string("node-set")
|
|
case .boolean: .string("boolean")
|
|
case .number: .string("number")
|
|
case .string, .none: .string("string")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: math
|
|
|
|
private static func mathFunction(_ local: String) -> FunctionImplementation? {
|
|
switch local {
|
|
case "min": { arguments, _ in .number(extremum(arguments.first, isMin: true)) }
|
|
case "max": { arguments, _ in .number(extremum(arguments.first, isMin: false)) }
|
|
case "highest": { arguments, _ in .nodeSet(extremeNodes(arguments.first, isMin: false)) }
|
|
case "lowest": { arguments, _ in .nodeSet(extremeNodes(arguments.first, isMin: true)) }
|
|
case "abs": { arguments, _ in .number(abs(arguments.first?.number ?? .nan)) }
|
|
case "sqrt": { arguments, _ in .number((arguments.first?.number ?? .nan).squareRoot()) }
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
/// The minimum or maximum of a node-set's numeric values; NaN for an empty
|
|
/// set or any non-numeric node (the EXSLT rule).
|
|
private static func extremum(_ value: Value?, isMin: Bool) -> Double {
|
|
guard let nodes = value?.nodes, !nodes.isEmpty else { return .nan }
|
|
let numbers = nodes.map { PureXML.XPath.Value.parseNumber($0.stringValue) }
|
|
if numbers.contains(where: \.isNaN) {
|
|
return .nan
|
|
}
|
|
return isMin ? numbers.min() ?? .nan : numbers.max() ?? .nan
|
|
}
|
|
|
|
/// The nodes whose numeric value equals the set's maximum (or minimum),
|
|
/// in document order; empty when any value is NaN.
|
|
private static func extremeNodes(_ value: Value?, isMin: Bool) -> [Node] {
|
|
guard let nodes = value?.nodes, !nodes.isEmpty else { return [] }
|
|
let target = extremum(value, isMin: isMin)
|
|
guard !target.isNaN else { return [] }
|
|
return nodes
|
|
.filter { PureXML.XPath.Value.parseNumber($0.stringValue) == target }
|
|
.sortedByDocumentOrder()
|
|
}
|
|
|
|
// MARK: sets
|
|
|
|
private static func setsFunction(_ local: String) -> FunctionImplementation? {
|
|
switch local {
|
|
case "distinct": { arguments, _ in .nodeSet(distinct(arguments.first)) }
|
|
case "difference": { arguments, _ in .nodeSet(difference(arguments)) }
|
|
case "intersection": { arguments, _ in .nodeSet(intersection(arguments)) }
|
|
case "has-same-node": { arguments, _ in .boolean(hasSameNode(arguments)) }
|
|
case "leading": { arguments, _ in .nodeSet(relative(arguments, leading: true)) }
|
|
case "trailing": { arguments, _ in .nodeSet(relative(arguments, leading: false)) }
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
private static func distinct(_ value: Value?) -> [Node] {
|
|
guard let nodes = value?.nodes else { return [] }
|
|
var seen: Set<String> = []
|
|
var result: [Node] = []
|
|
for node in nodes.sortedByDocumentOrder() where seen.insert(node.stringValue).inserted {
|
|
result.append(node)
|
|
}
|
|
return result
|
|
}
|
|
|
|
private static func difference(_ arguments: [Value]) -> [Node] {
|
|
let first = arguments.first?.nodes ?? []
|
|
let second = Set(arguments.count > 1 ? arguments[1].nodes ?? [] : [])
|
|
return first.filter { !second.contains($0) }.sortedByDocumentOrder()
|
|
}
|
|
|
|
private static func intersection(_ arguments: [Value]) -> [Node] {
|
|
let first = arguments.first?.nodes ?? []
|
|
let second = Set(arguments.count > 1 ? arguments[1].nodes ?? [] : [])
|
|
return first.filter { second.contains($0) }.sortedByDocumentOrder()
|
|
}
|
|
|
|
private static func hasSameNode(_ arguments: [Value]) -> Bool {
|
|
let second = Set(arguments.count > 1 ? arguments[1].nodes ?? [] : [])
|
|
return (arguments.first?.nodes ?? []).contains { second.contains($0) }
|
|
}
|
|
|
|
/// `set:leading` / `set:trailing`: the nodes of the first set that precede
|
|
/// (or follow) the first node, in document order, of the second set.
|
|
private static func relative(_ arguments: [Value], leading: Bool) -> [Node] {
|
|
let first = arguments.first?.nodes ?? []
|
|
guard let pivot = (arguments.count > 1 ? arguments[1].nodes ?? [] : []).firstInDocumentOrder() else {
|
|
return []
|
|
}
|
|
// Decorate once through a shared sibling-index cache: each node's
|
|
// order key is computed a single time, and the per-parent index is
|
|
// built once rather than rescanned for every node (which made this
|
|
// quadratic over a wide fan-out).
|
|
let cache = PureXML.XPath.SiblingIndexCache()
|
|
let pivotKey = pivot.documentOrder(cache: cache)
|
|
return first
|
|
.map { (node: $0, key: $0.documentOrder(cache: cache)) }
|
|
.filter { leading ? Node.ordered($0.key, before: pivotKey) : Node.ordered(pivotKey, before: $0.key) }
|
|
.sorted { Node.ordered($0.key, before: $1.key) }
|
|
.map(\.node)
|
|
}
|
|
}
|
|
}
|