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.
120 lines
5.1 KiB
Swift
120 lines
5.1 KiB
Swift
extension PureXML.Schema {
|
|
/// The outcome of comparing two durations, which form a partial order.
|
|
enum DurationOrder { case lessThan, equal, greaterThan, incomparable }
|
|
|
|
/// An `xs:duration` as its two independent signed components, months and
|
|
/// seconds, with the XSD 1.0 order relation. Because a month is 28 to 31
|
|
/// days, the order is partial: P and Q compare by adding each to the four
|
|
/// reference dateTimes the spec fixes, and durations whose sign disagrees
|
|
/// across those references are incomparable (for example `P1M` and `P30D`).
|
|
/// See XSD 1.0 Datatypes 3.2.6.2. Used by the ordering facets on duration,
|
|
/// which the generic total-order path cannot represent.
|
|
struct DurationValue {
|
|
let months: Int
|
|
let seconds: Double
|
|
|
|
/// The four reference points (year, month); each is the first of its
|
|
/// month at T00:00:00Z, so the day is always 1 and need not be carried.
|
|
private static let references: [(year: Int, month: Int)] = [
|
|
(1696, 9), (1697, 2), (1903, 3), (1903, 7),
|
|
]
|
|
|
|
func compare(to other: DurationValue) -> DurationOrder {
|
|
var sawLess = false
|
|
var sawGreater = false
|
|
var sawEqual = false
|
|
for reference in Self.references {
|
|
let here = Self.instant(reference, addingMonths: months, seconds: seconds)
|
|
let there = Self.instant(reference, addingMonths: other.months, seconds: other.seconds)
|
|
if here < there {
|
|
sawLess = true
|
|
} else if here > there {
|
|
sawGreater = true
|
|
} else {
|
|
sawEqual = true
|
|
}
|
|
}
|
|
// P < Q only when s+P < s+Q at every reference; > likewise; equal
|
|
// when all four coincide (which holds iff the components are equal).
|
|
if sawLess, !sawGreater, !sawEqual {
|
|
return .lessThan
|
|
}
|
|
if sawGreater, !sawLess, !sawEqual {
|
|
return .greaterThan
|
|
}
|
|
if sawEqual, !sawLess, !sawGreater {
|
|
return .equal
|
|
}
|
|
return .incomparable
|
|
}
|
|
|
|
/// The instant, in seconds, of `reference` + (months, seconds): add the
|
|
/// months with calendar rollover, then add the seconds. The reference
|
|
/// day is always 1, which is valid in every month, so no day clamping is
|
|
/// needed (a duration's day component lives in `seconds`, not here).
|
|
private static func instant(
|
|
_ reference: (year: Int, month: Int),
|
|
addingMonths addMonths: Int,
|
|
seconds: Double,
|
|
) -> Double {
|
|
let total = (reference.month - 1) + addMonths
|
|
let yearsCarried = Int((Double(total) / 12.0).rounded(.down))
|
|
let newYear = reference.year + yearsCarried
|
|
let newMonth = total - yearsCarried * 12 + 1
|
|
let days = DateTimeValue.daysFromCivil(year: newYear, month: newMonth, day: 1)
|
|
return Double(days) * 86400 + seconds
|
|
}
|
|
|
|
/// Parses the lexical form `-?P(nY)?(nM)?(nD)?(T(nH)?(nM)?(nS)?)?`. The
|
|
/// lexical recognizer has already accepted the value, so this trusts the
|
|
/// shape and only reads out the components.
|
|
init?(_ string: String) {
|
|
var characters = Substring(string)
|
|
var sign = 1.0
|
|
if characters.first == "-" {
|
|
sign = -1
|
|
characters = characters.dropFirst()
|
|
}
|
|
guard characters.first == "P" else { return nil }
|
|
characters = characters.dropFirst()
|
|
|
|
var inTime = false
|
|
var years = 0
|
|
var monthsPart = 0
|
|
var days = 0
|
|
var secondsPart = 0.0
|
|
var sawComponent = false
|
|
var number = ""
|
|
|
|
func consume(_ designator: Character) -> Bool {
|
|
guard !number.isEmpty, let magnitude = Double(number) else { return false }
|
|
switch (inTime, designator) {
|
|
case (false, "Y"): years = Int(magnitude)
|
|
case (false, "M"): monthsPart = Int(magnitude)
|
|
case (false, "D"): days = Int(magnitude)
|
|
case (true, "H"): secondsPart += magnitude * 3600
|
|
case (true, "M"): secondsPart += magnitude * 60
|
|
case (true, "S"): secondsPart += magnitude
|
|
default: return false
|
|
}
|
|
number = ""
|
|
sawComponent = true
|
|
return true
|
|
}
|
|
|
|
for character in characters {
|
|
if character == "T" {
|
|
inTime = true
|
|
} else if character.isNumber || character == "." {
|
|
number.append(character)
|
|
} else if !consume(character) {
|
|
return nil
|
|
}
|
|
}
|
|
guard sawComponent, number.isEmpty else { return nil }
|
|
months = Int(sign) * (years * 12 + monthsPart)
|
|
seconds = sign * (Double(days) * 86400 + secondsPart)
|
|
}
|
|
}
|
|
}
|