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.
99 lines
4.2 KiB
Swift
99 lines
4.2 KiB
Swift
extension PureXML.Schema {
|
|
/// An exact decimal value, parsed from an XSD `decimal`/`integer` lexical form
|
|
/// and compared without floating-point loss. Holds the sign and the
|
|
/// significant integer and fraction digits (leading and trailing zeros
|
|
/// removed), which is also what `totalDigits` and `fractionDigits` count.
|
|
struct DecimalValue: Equatable, Comparable, Sendable {
|
|
let negative: Bool
|
|
let integerDigits: [Character]
|
|
let fractionDigits: [Character]
|
|
|
|
/// Parses a decimal lexical form. With `allowFraction` false the value must
|
|
/// be an integer (no decimal point).
|
|
init?(_ lexical: String, allowFraction: Bool) {
|
|
var characters = Array(lexical)
|
|
guard !characters.isEmpty else { return nil }
|
|
|
|
var negative = false
|
|
if characters.first == "+" || characters.first == "-" {
|
|
negative = characters.first == "-"
|
|
characters.removeFirst()
|
|
}
|
|
guard !characters.isEmpty else { return nil }
|
|
|
|
let parts = Self.split(characters)
|
|
guard let (whole, fraction) = parts, allowFraction || fraction == nil else { return nil }
|
|
guard whole.allSatisfy(\.isNumber), (fraction ?? []).allSatisfy(\.isNumber) else { return nil }
|
|
guard !whole.isEmpty || !(fraction ?? []).isEmpty else { return nil }
|
|
|
|
let integerDigits = Self.dropLeadingZeros(whole)
|
|
let fractionDigits = Self.dropTrailingZeros(fraction ?? [])
|
|
self.integerDigits = integerDigits
|
|
self.fractionDigits = fractionDigits
|
|
// Negative zero is the same value as zero.
|
|
self.negative = (integerDigits.isEmpty && fractionDigits.isEmpty) ? false : negative
|
|
}
|
|
|
|
/// Splits the digits on a single decimal point, rejecting a second point.
|
|
/// Returns nil when malformed.
|
|
private static func split(_ characters: [Character]) -> (whole: [Character], fraction: [Character]?)? {
|
|
guard let dot = characters.firstIndex(of: ".") else { return (characters, nil) }
|
|
let rest = characters[(dot + 1)...]
|
|
guard !rest.contains(".") else { return nil }
|
|
return (Array(characters[..<dot]), Array(rest))
|
|
}
|
|
|
|
private static func dropLeadingZeros(_ digits: [Character]) -> [Character] {
|
|
Array(digits.drop { $0 == "0" })
|
|
}
|
|
|
|
private static func dropTrailingZeros(_ digits: [Character]) -> [Character] {
|
|
var digits = digits
|
|
while digits.last == "0" {
|
|
digits.removeLast()
|
|
}
|
|
return digits
|
|
}
|
|
|
|
var isZero: Bool {
|
|
integerDigits.isEmpty && fractionDigits.isEmpty
|
|
}
|
|
|
|
/// The XSD `totalDigits` of the value: every significant digit, at least one.
|
|
var totalDigits: Int {
|
|
Swift.max(1, integerDigits.count + fractionDigits.count)
|
|
}
|
|
|
|
static func < (lhs: DecimalValue, rhs: DecimalValue) -> Bool {
|
|
if lhs.negative != rhs.negative {
|
|
return lhs.negative
|
|
}
|
|
let magnitude = compareMagnitude(lhs, rhs)
|
|
return lhs.negative ? magnitude > 0 : magnitude < 0
|
|
}
|
|
|
|
/// Negative when `lhs` has the smaller magnitude, positive when larger.
|
|
private static func compareMagnitude(_ lhs: DecimalValue, _ rhs: DecimalValue) -> Int {
|
|
if lhs.integerDigits.count != rhs.integerDigits.count {
|
|
return lhs.integerDigits.count < rhs.integerDigits.count ? -1 : 1
|
|
}
|
|
if let order = lexicographic(lhs.integerDigits, rhs.integerDigits) {
|
|
return order
|
|
}
|
|
return lexicographic(lhs.fractionDigits, rhs.fractionDigits) ?? 0
|
|
}
|
|
|
|
/// Digit-by-digit comparison; a proper prefix is the smaller value. Returns
|
|
/// nil when the sequences are identical.
|
|
private static func lexicographic(_ lhs: [Character], _ rhs: [Character]) -> Int? {
|
|
for (left, right) in zip(lhs, rhs) where left != right {
|
|
return left < right ? -1 : 1
|
|
}
|
|
if lhs.count == rhs.count {
|
|
return nil
|
|
}
|
|
return lhs.count < rhs.count ? -1 : 1
|
|
}
|
|
}
|
|
}
|