Resolve a built-in datatype named by xsi:type (xsd:int, xsd:decimal, xsd:string), which previously resolved only against schema-declared types and so reported "unknown" and wrongly rejected the element. The built-in is now resolved when it validly substitutes for the declared type per Schema-Validity Assessment: the ur-type (anyType/anySimpleType) admits any type, and a bare built-in declared type admits any built-in derived from it through the XSD Part 2 datatype lattice (new BuiltinType.derives(from:)). An invalid built-in substitution is a located error, not a silent acceptance. Resolving the override correctly exposed two pre-existing gaps, both closed so the change carries no invalid-instances-accepted regression: - A simple-typed element rejects non-schema-instance attributes. A simple type declares no attribute uses, so an element of simple type (including via an xsi:type override, and including an xsi:nil element) may carry only xsi:* and namespace attributes. Checked before the nil short-circuit. - Identity-constraint fields compare in their value space when the selected node carries an xsi:type. unique/key/keyref tuples compared lexically, so two xsi:type="xsd:decimal" fields 3.0 and 3 were treated as distinct and a unique violation was missed. Each field value now carries the type read from the node's xsi:type and compares via SimpleType.valueMatches when both share a built-in base, otherwise lexically. XSTS: valid instances rejected 248 to 233; invalid instances accepted held at 165. Identity tests corrected to spec-valid schemas (an element carrying an attribute must be a complex type, not type="xs:string").
166 lines
5.9 KiB
Swift
166 lines
5.9 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
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|