PureXML/Sources/Schema/BuiltinType.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

128 lines
4.4 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"
var whiteSpace: WhiteSpace {
switch self {
case .string: .preserve
case .normalizedString: .replace
default: .collapse
}
}
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
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)
}
}
}