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

49 lines
2.5 KiB
Swift

extension PureXML.Schema.IdentityValidator {
/// The identity value a field takes from a declared `default`/`fixed` when the
/// instance supplies none, or nil when the instance supplies a value (the
/// caller then uses it). The substituted value carries the field's resolved
/// type so it compares in the same value space as an explicit one.
func defaultedValue(
_ field: String,
value: PureXML.XPath.Value,
type: PureXML.Schema.SimpleType?,
constraint: PureXML.Schema.IdentityConstraint,
at node: PureXML.Model.TreeNode,
) -> FieldValue? {
guard isFieldEmpty(field, value: value),
let constrained = resolveFieldConstraint(field, constraint: constraint, at: node)
else { return nil }
return FieldValue(string: constrained.value, type: type, namespaceBindings: namespaceBindings(at: node))
}
/// Whether a field has no instance-supplied value, so a declared
/// `default`/`fixed` may stand in. An `@attr` field is empty only when the
/// attribute is ABSENT (empty node-set); a present attribute, even value "",
/// is its own value. A `.` field is empty when the selected element has no
/// character content; any other field is empty only on an empty node-set.
func isFieldEmpty(_ field: String, value: PureXML.XPath.Value) -> Bool {
guard let nodes = value.nodes else { return false }
if field.hasPrefix("@") { return nodes.isEmpty }
if field == "." {
guard case let .tree(element)? = nodes.first, element.kind == .element else { return nodes.isEmpty }
return !element.children.contains { $0.kind == .text || $0.kind == .cdata }
}
return nodes.isEmpty
}
/// The `default`/`fixed` declared on a field's target, keyed the same way as
/// ``resolveFieldType(_:constraint:at:)`` so a multi-branch `.` selector
/// resolves to its branch's element.
func resolveFieldConstraint(
_ field: String,
constraint: PureXML.Schema.IdentityConstraint,
at target: PureXML.Model.TreeNode,
) -> PureXML.Schema.ValueConstraint? {
if field == ".", let local = target.name?.localName {
let key = PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field, targetLocal: local)
if let value = fieldConstraints[key] { return value }
}
return fieldConstraints[PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field)]
}
}