An identity-constraint (unique/key/keyref) field that selects an absent attribute or an empty element whose declaration carries a default or fixed value now takes that value as its identity-tuple component, so two members can collide on the defaulted value (idG011/idG012 default, idF016/idF017 fixed) where the duplicate was previously missed. The default/fixed value constraints on identity-field targets are gathered at compile time (identityFieldConstraints, mirroring identityFieldTypes) and threaded through to the identity validator. The substitution is exact about absent versus present-empty: an @attr field is defaulted only when the attribute is ABSENT (an empty node-set), never when present with value ""; a "." element field only when the selected element has no character content. Applying a default only adds an identity value where the schema declares one, so it can create a genuine duplicate-key detection but never reject a valid instance. To keep within the file/type-size and one-type-per-file limits, FieldValue, XPathQueryCache, and the schema-location hints were extracted to their own files (behavior-preserving moves). invalid-instances-accepted 50 to 45, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. An adversarial review probed present-empty attributes, element content winning over a default, fixed values, keyref matching, multi-field keys, and merged-schema defaults, and found no over-rejection.
57 lines
2.5 KiB
Swift
57 lines
2.5 KiB
Swift
/// One field of an identity tuple: its lexical value and the simple type it
|
|
/// compares in (from the node's `xsi:type`, if any). Two field values are equal
|
|
/// when they denote the same value: in the value space of their type when both
|
|
/// share a built-in base (so `3.0` equals `3` for `xsd:decimal`), otherwise by
|
|
/// lexical form.
|
|
struct FieldValue: Equatable {
|
|
let string: String
|
|
let type: PureXML.Schema.SimpleType?
|
|
let namespaceBindings: [String: String]
|
|
|
|
init(string: String, type: PureXML.Schema.SimpleType?, namespaceBindings: [String: String] = [:]) {
|
|
self.string = string
|
|
self.type = type
|
|
self.namespaceBindings = namespaceBindings
|
|
}
|
|
|
|
static func == (lhs: FieldValue, rhs: FieldValue) -> Bool {
|
|
if let left = lhs.qnameValue, let right = rhs.qnameValue, left.uri == right.uri, left.local == right.local {
|
|
return true
|
|
}
|
|
if let lhsType = lhs.type, let rhsType = rhs.type {
|
|
if lhsType.base == rhsType.base {
|
|
return lhsType.valueMatches(lhs.string, literal: rhs.string)
|
|
}
|
|
return Self.numericallyEqual(lhs.string, lhsType.base, rhs.string, rhsType.base)
|
|
}
|
|
return lhs.string == rhs.string
|
|
}
|
|
|
|
/// Whether two values denote the same number across the decimal-derived
|
|
/// numeric family. `decimal` and the integer types share one value space, so
|
|
/// `1` (decimal) equals `1` (unsignedByte). Distinct primitive spaces (a
|
|
/// number versus a string, or `float`/`double`) are excluded, so they never
|
|
/// compare equal here.
|
|
private static func numericallyEqual(
|
|
_ left: String,
|
|
_ leftBase: PureXML.Schema.BuiltinType,
|
|
_ right: String,
|
|
_ rightBase: PureXML.Schema.BuiltinType,
|
|
) -> Bool {
|
|
guard leftBase.isDecimalDerived, rightBase.isDecimalDerived,
|
|
let leftValue = PureXML.Schema.DecimalValue(left, allowFraction: true),
|
|
let rightValue = PureXML.Schema.DecimalValue(right, allowFraction: true)
|
|
else { return false }
|
|
return leftValue == rightValue
|
|
}
|
|
|
|
private var qnameValue: (uri: String?, local: String)? {
|
|
let trimmed = string.trimmingXMLWhitespace()
|
|
guard trimmed.contains(":") else { return nil }
|
|
let parts = trimmed.split(separator: ":", maxSplits: 1).map(String.init)
|
|
guard parts.count == 2 else { return nil }
|
|
let uri = namespaceBindings[parts[0]] ?? (parts[0].isEmpty ? namespaceBindings[""] : nil)
|
|
return (uri, parts[1])
|
|
}
|
|
}
|