PureXML/Sources/Schema/IdentityFieldValue.swift
Mihaela Mihaljevic ca56caf11c fix(schema): default/fixed value for identity-constraint fields; invalid-instances-accepted 50 to 45 (#147)
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.
2026-06-18 14:47:47 +02:00

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])
}
}