PureXML/Sources/Schema/Primitive.swift
Mihaela Mihaljevic e862f364aa feat(schema): XSD simple-type datatype library with facets
Add the PureXML.Schema namespace validating XSD Part 2 built-in datatypes
and constraining facets. Built-ins span string/boolean/decimal, the bounded
integer family with intrinsic bounds, float/double, duration, the eight
date/time types, hexBinary, base64Binary, anyURI, QName, and the
Name/NCName/NMTOKEN/language family. Facets: length/minLength/maxLength,
pattern (via the regex engine), enumeration (value-space comparison),
whiteSpace, min/maxInclusive/Exclusive, totalDigits, fractionDigits.
Decimals compare exactly with no floating-point loss; date/time fields are
range-validated with leap years, 24:00:00, and timezone bounds.

Closes #8
2026-06-08 06:29:37 +02:00

107 lines
3.8 KiB
Swift

extension PureXML.Schema {
/// A value compared for the ordering facets (`minInclusive` and friends).
enum OrderedValue: Equatable, Comparable {
case decimal(DecimalValue)
case double(Double)
case dateTime(DateTimeValue)
static func < (lhs: OrderedValue, rhs: OrderedValue) -> Bool {
switch (lhs, rhs) {
case let (.decimal(left), .decimal(right)): left < right
case let (.double(left), .double(right)): left < right
case let (.dateTime(left), .dateTime(right)): left < right
default: false
}
}
}
/// An XSD primitive value space: the lexical recognizer, and for the ordered
/// spaces a value usable by the ordering facets.
enum Primitive: Sendable {
case string
case boolean
case decimal
case integer
case double
case float
case duration
case dateKind(DateKind)
case hexBinary
case base64Binary
case anyURI
case qName
case name
case ncName
case nmtoken
case language
func isValid(_ value: String) -> Bool {
stringValidity(value) ?? numericValidity(value)
}
/// Validity for the string-like and boolean spaces; nil when this primitive
/// is handled by ``numericValidity(_:)``.
private func stringValidity(_ value: String) -> Bool? {
switch self {
case .string, .anyURI: true
case .boolean: Lexical.isBoolean(value)
case .qName: Lexical.isQName(value)
case .name: Lexical.isName(value)
case .ncName: Lexical.isNCName(value)
case .nmtoken: Lexical.isNMToken(value)
case .language: Lexical.isLanguage(value)
default: nil
}
}
private func numericValidity(_ value: String) -> Bool {
switch self {
case .decimal: DecimalValue(value, allowFraction: true) != nil
case .integer: DecimalValue(value, allowFraction: false) != nil
case .double, .float: Lexical.isFloating(value)
case .duration: Lexical.isDuration(value)
case let .dateKind(kind): DateTimeParser.parse(value, kind: kind) != nil
case .hexBinary: Lexical.isHexBinary(value)
case .base64Binary: Lexical.isBase64Binary(value)
default: false
}
}
/// The ordering value for a lexically valid string, or nil for the
/// unordered spaces.
func ordered(_ value: String) -> OrderedValue? {
switch self {
case .decimal: DecimalValue(value, allowFraction: true).map(OrderedValue.decimal)
case .integer: DecimalValue(value, allowFraction: false).map(OrderedValue.decimal)
case .double, .float: Self.floatingValue(value).map(OrderedValue.double)
case let .dateKind(kind): DateTimeParser.parse(value, kind: kind).map(OrderedValue.dateTime)
default: nil
}
}
private static func floatingValue(_ value: String) -> Double? {
switch value {
case "INF": .infinity
case "-INF": -.infinity
case "NaN": .nan
default: Double(value)
}
}
/// The unit `length`, `minLength`, and `maxLength` count: octets for binary
/// types, characters otherwise.
func measuredLength(_ value: String) -> Int {
switch self {
case .hexBinary: value.count / 2
case .base64Binary: Self.base64Octets(value)
default: value.count
}
}
private static func base64Octets(_ value: String) -> Int {
let padding = value.reversed().prefix { $0 == "=" }.count
return value.count / 4 * 3 - padding
}
}
}