PureXML/Sources/Schema/OccurrenceRange.swift
Mihaela Mihaljevic 380160cc2b refactor(validation): decompose XSD instance validation into composable rules
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.
2026-07-12 14:12:17 +02:00

115 lines
3.6 KiB
Swift

public extension PureXML.Schema {
/// A non-negative integer magnitude preserved as normalized decimal text.
///
/// XSD occurrence attributes are `nonNegativeInteger` values and may exceed
/// the platform `Int` range. This type compares such values without converting
/// them to machine integers.
struct NonNegativeDecimal: Sendable, Hashable, Comparable, CustomStringConvertible {
private var digits: String
public init(_ value: Int) {
precondition(value >= 0, "occurrence values must be non-negative")
digits = String(value)
}
public init?(lexical value: String) {
var raw = Substring(value.trimmingXMLWhitespace())
if raw.first == "+" {
raw = raw.dropFirst()
}
guard !raw.isEmpty, raw.allSatisfy({ $0.isASCII && $0.isNumber }) else {
return nil
}
let trimmed = raw.drop { $0 == "0" }
digits = trimmed.isEmpty ? "0" : String(trimmed)
}
public var description: String {
digits
}
var isZero: Bool {
digits == "0"
}
func clamped(to limit: Int) -> Int {
precondition(limit >= 0, "occurrence clamp limit must be non-negative")
if isGreaterThan(limit) {
return limit
}
return Int(digits) ?? limit
}
func isGreaterThan(_ value: Int) -> Bool {
precondition(value >= 0, "occurrence comparison value must be non-negative")
let other = String(value)
if digits.count != other.count {
return digits.count > other.count
}
return digits > other
}
func isLessThanOrEqual(to value: Int) -> Bool {
!isGreaterThan(value)
}
public static func < (lhs: Self, rhs: Self) -> Bool {
if lhs.digits.count != rhs.digits.count {
return lhs.digits.count < rhs.digits.count
}
return lhs.digits < rhs.digits
}
}
/// The upper occurrence bound of an XSD particle.
enum OccurrenceUpper: Sendable, Hashable, Equatable {
case finite(NonNegativeDecimal)
case unbounded
public init(_ value: Int?) {
if let value {
self = .finite(NonNegativeDecimal(value))
} else {
self = .unbounded
}
}
public var isZero: Bool {
if case let .finite(value) = self {
return value.isZero
}
return false
}
func clamped(to limit: Int) -> Int? {
switch self {
case let .finite(value): value.clamped(to: limit)
case .unbounded: nil
}
}
func isGreaterThan(_ value: Int) -> Bool {
switch self {
case let .finite(upper): upper.isGreaterThan(value)
case .unbounded: true
}
}
}
/// The full occurrence range of an XSD particle.
struct OccurrenceRange: Sendable, Hashable, Equatable {
public var minimum: NonNegativeDecimal
public var maximum: OccurrenceUpper
public init(minimum: NonNegativeDecimal = NonNegativeDecimal(1), maximum: OccurrenceUpper = .finite(NonNegativeDecimal(1))) {
self.minimum = minimum
self.maximum = maximum
}
public init(minimum: Int = 1, maximum: Int? = 1) {
self.minimum = NonNegativeDecimal(minimum)
self.maximum = OccurrenceUpper(maximum)
}
}
}