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.
183 lines
6.6 KiB
Swift
183 lines
6.6 KiB
Swift
public extension PureXML.Schema {
|
|
/// Whitespace processing applied before a value is validated.
|
|
enum WhiteSpace: Sendable {
|
|
case preserve
|
|
case replace
|
|
case collapse
|
|
}
|
|
|
|
/// A built-in XSD datatype. Each carries its primitive value space, its
|
|
/// intrinsic `whiteSpace`, and, for the bounded integer types, its inclusive
|
|
/// bounds, exactly as XSD Part 2 derives them.
|
|
enum BuiltinType: String, Sendable, CaseIterable {
|
|
case string
|
|
case normalizedString
|
|
case token
|
|
case language
|
|
case name = "Name"
|
|
case ncName = "NCName"
|
|
case nmtoken = "NMTOKEN"
|
|
case id = "ID"
|
|
case idref = "IDREF"
|
|
case entity = "ENTITY"
|
|
case boolean
|
|
case decimal
|
|
case integer
|
|
case long
|
|
case int
|
|
case short
|
|
case byte
|
|
case nonNegativeInteger
|
|
case positiveInteger
|
|
case nonPositiveInteger
|
|
case negativeInteger
|
|
case unsignedLong
|
|
case unsignedInt
|
|
case unsignedShort
|
|
case unsignedByte
|
|
case float
|
|
case double
|
|
case duration
|
|
case dateTime
|
|
case date
|
|
case time
|
|
case gYearMonth
|
|
case gYear
|
|
case gMonthDay
|
|
case gDay
|
|
case gMonth
|
|
case hexBinary
|
|
case base64Binary
|
|
case anyURI
|
|
case qName = "QName"
|
|
case notation = "NOTATION"
|
|
|
|
var whiteSpace: WhiteSpace {
|
|
switch self {
|
|
case .string: .preserve
|
|
case .normalizedString: .replace
|
|
default: .collapse
|
|
}
|
|
}
|
|
|
|
/// The built-in this type is directly derived from in the XSD Part 2
|
|
/// datatype lattice, or nil for the primitives whose base is the implicit
|
|
/// root `anySimpleType`.
|
|
var derivationBase: BuiltinType? {
|
|
switch self {
|
|
case .normalizedString: .string
|
|
case .token: .normalizedString
|
|
case .language, .name, .nmtoken: .token
|
|
case .ncName: .name
|
|
case .id, .idref, .entity: .ncName
|
|
case .integer: .decimal
|
|
case .long: .integer
|
|
case .int: .long
|
|
case .short: .int
|
|
case .byte: .short
|
|
case .nonNegativeInteger, .nonPositiveInteger: .integer
|
|
case .positiveInteger: .nonNegativeInteger
|
|
case .negativeInteger: .nonPositiveInteger
|
|
case .unsignedLong: .nonNegativeInteger
|
|
case .unsignedInt: .unsignedLong
|
|
case .unsignedShort: .unsignedInt
|
|
case .unsignedByte: .unsignedShort
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
/// Whether this type is `other` or is derived from it through the built-in
|
|
/// lattice. Used to check an `xsi:type` built-in validly substitutes for a
|
|
/// built-in declared type: a derived type may always stand in for its base.
|
|
func derives(from other: BuiltinType) -> Bool {
|
|
var current: BuiltinType? = self
|
|
while let type = current {
|
|
if type == other {
|
|
return true
|
|
}
|
|
current = type.derivationBase
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Whether this type's value space is the `decimal` value space: `decimal`
|
|
/// itself or any of the integer family (which derive from it). Values from
|
|
/// two such types compare in the one shared value space, so `1` (decimal)
|
|
/// equals `1` (unsignedByte). `float` and `double` are distinct primitive
|
|
/// value spaces and are deliberately excluded.
|
|
var isDecimalDerived: Bool {
|
|
self == .decimal || Self.integerLike.contains(self)
|
|
}
|
|
|
|
var primitive: Primitive {
|
|
if let primitive = Self.stringLike[self] {
|
|
return primitive
|
|
}
|
|
if Self.integerLike.contains(self) {
|
|
return .integer
|
|
}
|
|
return computedPrimitive
|
|
}
|
|
|
|
private var computedPrimitive: Primitive {
|
|
switch self {
|
|
case .boolean: .boolean
|
|
case .decimal: .decimal
|
|
case .float: .float
|
|
case .double: .double
|
|
case .duration: .duration
|
|
case .dateTime: .dateKind(.dateTime)
|
|
case .date: .dateKind(.date)
|
|
case .time: .dateKind(.time)
|
|
case .gYearMonth: .dateKind(.gYearMonth)
|
|
case .gYear: .dateKind(.gYear)
|
|
case .gMonthDay: .dateKind(.gMonthDay)
|
|
case .gDay: .dateKind(.gDay)
|
|
case .gMonth: .dateKind(.gMonth)
|
|
case .hexBinary: .hexBinary
|
|
case .base64Binary: .base64Binary
|
|
case .anyURI: .anyURI
|
|
case .qName: .qName
|
|
case .notation: .notation
|
|
default: .string
|
|
}
|
|
}
|
|
|
|
private static let stringLike: [BuiltinType: Primitive] = [
|
|
.string: .string, .normalizedString: .string, .token: .string,
|
|
.language: .language, .name: .name,
|
|
.ncName: .ncName, .id: .ncName, .idref: .ncName, .entity: .ncName,
|
|
.nmtoken: .nmtoken,
|
|
]
|
|
|
|
private static let integerLike: Set<BuiltinType> = [
|
|
.integer, .long, .int, .short, .byte,
|
|
.nonNegativeInteger, .positiveInteger, .nonPositiveInteger, .negativeInteger,
|
|
.unsignedLong, .unsignedInt, .unsignedShort, .unsignedByte,
|
|
]
|
|
|
|
/// The intrinsic inclusive bounds of the bounded integer types.
|
|
var bounds: (lower: DecimalValue?, upper: DecimalValue?) {
|
|
switch self {
|
|
case .long: (Self.bound("-9223372036854775808"), Self.bound("9223372036854775807"))
|
|
case .int: (Self.bound("-2147483648"), Self.bound("2147483647"))
|
|
case .short: (Self.bound("-32768"), Self.bound("32767"))
|
|
case .byte: (Self.bound("-128"), Self.bound("127"))
|
|
case .unsignedLong: (Self.bound("0"), Self.bound("18446744073709551615"))
|
|
case .unsignedInt: (Self.bound("0"), Self.bound("4294967295"))
|
|
case .unsignedShort: (Self.bound("0"), Self.bound("65535"))
|
|
case .unsignedByte: (Self.bound("0"), Self.bound("255"))
|
|
case .nonNegativeInteger: (Self.bound("0"), nil)
|
|
case .positiveInteger: (Self.bound("1"), nil)
|
|
case .nonPositiveInteger: (nil, Self.bound("0"))
|
|
case .negativeInteger: (nil, Self.bound("-1"))
|
|
default: (nil, nil)
|
|
}
|
|
}
|
|
|
|
private static func bound(_ lexical: String) -> DecimalValue? {
|
|
DecimalValue(lexical, allowFraction: false)
|
|
}
|
|
}
|
|
}
|