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
125 lines
5.3 KiB
Swift
125 lines
5.3 KiB
Swift
@testable import PureXML
|
|
import Testing
|
|
|
|
@Suite("XSD datatypes")
|
|
struct XSDDatatypeTests {
|
|
private typealias Schema = PureXML.Schema
|
|
|
|
private func valid(_ value: String, _ type: Schema.BuiltinType) -> Bool {
|
|
Schema.isValid(value, type: type)
|
|
}
|
|
|
|
// MARK: Primitives
|
|
|
|
@Test("boolean accepts the four lexical forms")
|
|
func test_boolean() {
|
|
#expect(valid("true", .boolean) && valid("false", .boolean))
|
|
#expect(valid("0", .boolean) && valid("1", .boolean))
|
|
#expect(!valid("True", .boolean) && !valid("yes", .boolean))
|
|
}
|
|
|
|
@Test("decimal and integer lexical spaces")
|
|
func test_decimalInteger() {
|
|
#expect(valid("-12.345", .decimal) && valid("+.5", .decimal) && valid("100", .decimal))
|
|
#expect(!valid("1.2.3", .decimal) && !valid("1e3", .decimal))
|
|
#expect(valid("-42", .integer) && !valid("4.0", .integer))
|
|
}
|
|
|
|
@Test("double accepts exponents and the special values")
|
|
func test_double() {
|
|
#expect(valid("1.5e-9", .double) && valid("INF", .double) && valid("-INF", .double) && valid("NaN", .double))
|
|
#expect(!valid("1.5e", .double) && !valid("inf", .double))
|
|
}
|
|
|
|
@Test("bounded integer types enforce their ranges")
|
|
func test_integerBounds() {
|
|
#expect(valid("127", .byte) && !valid("128", .byte))
|
|
#expect(valid("-128", .byte) && !valid("-129", .byte))
|
|
#expect(valid("255", .unsignedByte) && !valid("-1", .unsignedByte))
|
|
#expect(valid("1", .positiveInteger) && !valid("0", .positiveInteger))
|
|
#expect(valid("0", .nonNegativeInteger) && !valid("-1", .nonNegativeInteger))
|
|
}
|
|
|
|
// MARK: Dates
|
|
|
|
@Test("date validates field ranges and leap years")
|
|
func test_date() {
|
|
#expect(valid("2024-02-29", .date) && !valid("2023-02-29", .date))
|
|
#expect(!valid("2024-02-30", .date) && !valid("2024-13-01", .date))
|
|
#expect(valid("-0044-03-15", .date))
|
|
}
|
|
|
|
@Test("dateTime accepts 24:00:00 but not 24:00:01, and timezones")
|
|
func test_dateTime() {
|
|
#expect(valid("2026-06-08T24:00:00", .dateTime))
|
|
#expect(!valid("2026-06-08T24:00:01", .dateTime))
|
|
#expect(valid("2026-06-08T12:30:00Z", .dateTime) && valid("2026-06-08T12:30:00+05:30", .dateTime))
|
|
#expect(!valid("2026-06-08T12:30:00+15:00", .dateTime))
|
|
}
|
|
|
|
@Test("the g* date types parse their reduced forms")
|
|
func test_gTypes() {
|
|
#expect(valid("--02-29", .gMonthDay) && !valid("--02-30", .gMonthDay))
|
|
#expect(valid("---31", .gDay) && valid("--12", .gMonth) && valid("2026", .gYear))
|
|
}
|
|
|
|
// MARK: Other primitives
|
|
|
|
@Test("hexBinary, base64Binary, and names")
|
|
func test_binaryAndNames() {
|
|
#expect(valid("0fB7", .hexBinary) && !valid("0fB", .hexBinary))
|
|
#expect(valid("YW55", .base64Binary) && valid("YW4=", .base64Binary))
|
|
#expect(valid("ns:local", .qName) && !valid("ns:1bad", .qName))
|
|
#expect(valid("Hello", .ncName) && !valid("a:b", .ncName))
|
|
#expect(valid("en-US", .language) && !valid("english-", .language))
|
|
#expect(valid("P1Y2M3DT4H", .duration) && !valid("P", .duration) && !valid("PT", .duration))
|
|
}
|
|
|
|
// MARK: Facets
|
|
|
|
@Test("whiteSpace collapse is applied before validation")
|
|
func test_whiteSpace() {
|
|
#expect(Schema.SimpleType(base: .token).isValid(" a b "))
|
|
#expect(Schema.SimpleType(base: .int).isValid(" 42 "))
|
|
}
|
|
|
|
@Test("length, minLength, and maxLength count characters")
|
|
func test_lengthFacets() {
|
|
let type = Schema.SimpleType(base: .string, facets: .init(minLength: 2, maxLength: 4))
|
|
#expect(type.isValid("abc") && !type.isValid("a") && !type.isValid("abcde"))
|
|
}
|
|
|
|
@Test("pattern facets use the regex engine")
|
|
func test_patternFacet() {
|
|
let type = Schema.SimpleType(base: .string, facets: .init(patterns: ["[A-Z]{3}-\\d{2}"]))
|
|
#expect(type.isValid("XYZ-42") && !type.isValid("xyz-42"))
|
|
}
|
|
|
|
@Test("enumeration compares in value space")
|
|
func test_enumerationFacet() {
|
|
let type = Schema.SimpleType(base: .decimal, facets: .init(enumeration: ["1.0", "2.5"]))
|
|
#expect(type.isValid("1.00") && type.isValid("2.5") && !type.isValid("3"))
|
|
}
|
|
|
|
@Test("inclusive and exclusive ranges")
|
|
func test_rangeFacets() {
|
|
let inclusive = Schema.SimpleType(base: .integer, facets: .init(minInclusive: "1", maxInclusive: "10"))
|
|
#expect(inclusive.isValid("1") && inclusive.isValid("10") && !inclusive.isValid("11"))
|
|
let exclusive = Schema.SimpleType(base: .integer, facets: .init(minExclusive: "1", maxExclusive: "10"))
|
|
#expect(!exclusive.isValid("1") && exclusive.isValid("2") && !exclusive.isValid("10"))
|
|
}
|
|
|
|
@Test("totalDigits and fractionDigits")
|
|
func test_digitFacets() {
|
|
let type = Schema.SimpleType(base: .decimal, facets: .init(totalDigits: 4, fractionDigits: 2))
|
|
#expect(type.isValid("12.34") && type.isValid("12") && !type.isValid("123.45") && !type.isValid("1.234"))
|
|
}
|
|
|
|
@Test("exact decimal comparison avoids floating-point error")
|
|
func test_exactDecimal() {
|
|
// 0.1 + 0.2 != 0.3 in binary; exact comparison handles boundary values.
|
|
let type = Schema.SimpleType(base: .decimal, facets: .init(maxInclusive: "0.3"))
|
|
#expect(type.isValid("0.3") && type.isValid("0.30") && !type.isValid("0.30000000001"))
|
|
}
|
|
}
|