extension PureXML.Schema { /// Validates XSD identity constraints (`unique`, `key`, `keyref`) over an /// instance document. Each constraint is evaluated at every element whose /// name carries it: the selector XPath chooses target nodes relative to that /// element, and the field XPaths form each target's value tuple. `key` and /// `unique` require distinct tuples; `keyref` requires each tuple to match a /// key or unique of the named constraint visible in an enclosing scope. struct IdentityValidator { /// The constraints declared for each element local name. let constraints: [String: [IdentityConstraint]] /// Simple types for identity-constraint fields keyed by /// ``XSDParser/identityFieldKey(selector:field:)``. let fieldTypes: [String: SimpleType] /// `default`/`fixed` value constraints on identity-constraint field /// targets, keyed the same way as ``fieldTypes``. When a field selects an /// absent attribute or an empty element whose declaration carries one of /// these, the field's identity value is that value, not nothing. let fieldConstraints: [String: PureXML.Schema.ValueConstraint] /// Keys (``XSDParser/identityFieldKey(constraint:field:)``) of fields whose /// declared target is not a simple type, computed at compile time, so the /// field violates cvc-identity-constraint.3 (XSTS idZ010). let nonSimpleFields: Set /// Global element and type declarations for runtime field typing. let types: [String: ElementType] /// Selector and field XPaths compiled once per run, not per element. private let cache = XPathQueryCache() /// Same-name sibling positions for error-location paths, indexed once per /// parent and name. ``steps`` looks a target's position up here instead of /// rescanning the parent's children every call, which made locating each /// of a wide list's targets O(n) and the whole `unique`/`key`/`keyref` /// pass quadratic. let stepIndex = StepSiblingIndex() init( constraints: [String: [IdentityConstraint]], fieldTypes: [String: SimpleType] = [:], fieldConstraints: [String: PureXML.Schema.ValueConstraint] = [:], nonSimpleFields: Set = [], types: [String: ElementType] = [:], ) { self.constraints = constraints self.fieldTypes = fieldTypes self.fieldConstraints = fieldConstraints self.nonSimpleFields = nonSimpleFields self.types = types } func validate( _ root: PureXML.Model.TreeNode, at path: [PureXML.Validation.PathKey] = [], ) -> [PureXML.Validation.ValidationError] { validateTagged(root, at: path).map(\.error) } /// The tagged form of ``validate(_:at:)``: the same computation, with each /// finding labeled by the decomposed identity rule it belongs to. The /// public, untagged form is a thin `.map(\.error)` over this so behavior and /// output stay identical for every existing caller. func validateTagged( _ root: PureXML.Model.TreeNode, at path: [PureXML.Validation.PathKey] = [], ) -> [XSDFinding] { guard !constraints.isEmpty else { return [] } // A selector or field XPath that does not compile is a schema-author // error, reported once up front rather than silently disabling the // constraint (the broken query still evaluates as no-match below). var issues: [XSDFinding] = compileErrors(at: path) walk(root, at: path, scopes: [], into: &issues) return issues } /// One located error per constraint XPath that fails to compile, in a /// deterministic order. The failed compilations are cached, so the walk /// afterwards treats those constraints as matching nothing. private func compileErrors(at path: [PureXML.Validation.PathKey]) -> [XSDFinding] { var errors: [XSDFinding] = [] for key in constraints.keys.sorted() { for constraint in constraints[key] ?? [] { if cache.query(constraint.selector) == nil { errors.append(XSDFinding( .identityConstraintXPathsValid, reason: "identity constraint '\(constraint.name)': invalid selector XPath '\(constraint.selector)'", at: path, )) } for field in constraint.fields where cache.query(field) == nil { errors.append(XSDFinding(.identityConstraintXPathsValid, reason: "identity constraint '\(constraint.name)': invalid field XPath '\(field)'", at: path)) } } } return errors } private func walk( _ node: PureXML.Model.TreeNode, at path: [PureXML.Validation.PathKey], scopes: [[String: [[FieldValue?]]]], into issues: inout [XSDFinding], ) { var frame: [String: [[FieldValue?]]] = [:] let declared = node.name.flatMap { constraints[$0.localName] } ?? [] // Collect key and unique tuples first so a keyref on the same element // can resolve against them. for constraint in declared { collect(constraint, at: node, path: path, frame: &frame, into: &issues) } let visible = scopes + [frame] for constraint in declared { if case .keyref = constraint.kind { checkKeyref(constraint, at: node, path: path, scopes: visible, into: &issues) } } let elementChildren = node.children.filter { $0.kind == .element } for (child, step) in zip(elementChildren, childSteps(elementChildren)) { walk(child, at: path + [step], scopes: visible, into: &issues) } } /// The coding-path step for each element child: its name, with a sibling /// index only when more than one child shares that name. Mirrors the /// complex-type validator's path construction so identity-constraint /// errors and content errors locate consistently. private func childSteps(_ children: [PureXML.Model.TreeNode]) -> [PureXML.Validation.PathKey] { PureXML.Validation.PathKey.steps(forChildNames: children.map { $0.name?.description ?? "" }) } /// The coding-path step from a `target` down to the single field a /// constraint names, so an error locates the offending value (`@id`, a /// child element) rather than just the target. Empty for a multi-field /// constraint or a complex field path, in which case the error stays on the /// target. private func fieldStep(_ fields: [String], at target: PureXML.Model.TreeNode, namespaces: [String: String]) -> [PureXML.Validation.PathKey] { guard fields.count == 1 else { return [] } let field = fields[0] if field.hasPrefix("@"), !field.dropFirst().contains(where: { $0 == "/" || $0 == "[" }) { return [.attribute(String(field.dropFirst()))] } let isSimpleName = !field.contains(where: { "/@[(".contains($0) }) if isSimpleName, let child = select(field, at: target, namespaces: namespaces).first, child.kind == .element { return steps(from: target, to: child) } return [] } private func collect( _ constraint: IdentityConstraint, at node: PureXML.Model.TreeNode, path: [PureXML.Validation.PathKey], frame: inout [String: [[FieldValue?]]], into issues: inout [XSDFinding], ) { guard isNonRef(constraint) else { return } var tuples: [[FieldValue?]] = [] var seen = TupleSet() for target in select(constraint.selector, at: node, namespaces: constraint.namespaceBindings) { let targetPath = path + steps(from: node, to: target) if let error = fieldError(constraint, at: target, path: targetPath) { issues.append(error) continue } let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint) if constraint.kind == .key, tuple.contains(where: { $0 == nil }) { issues.append(XSDFinding( .identityKeyFieldPresent, reason: "key '\(constraint.name)': a field is missing", at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings), )) continue } if tuple.contains(where: { $0 == nil }) { continue } if seen.contains(tuple) { issues.append(XSDFinding( .identityValueUnique, reason: "\(label(constraint)) '\(constraint.name)': duplicate value", at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings), )) } else { seen.insert(tuple) } tuples.append(tuple) } frame[constraint.name] = tuples } private func checkKeyref( _ constraint: IdentityConstraint, at node: PureXML.Model.TreeNode, path: [PureXML.Validation.PathKey], scopes: [[String: [[FieldValue?]]]], into issues: inout [XSDFinding], ) { guard case let .keyref(refer) = constraint.kind else { return } let keyTuples = scopes.reversed().compactMap { $0[refer] }.first ?? [] var keys = TupleSet() for keyTuple in keyTuples { keys.insert(keyTuple) } for target in select(constraint.selector, at: node, namespaces: constraint.namespaceBindings) { let targetPath = path + steps(from: node, to: target) if let error = fieldError(constraint, at: target, path: targetPath) { issues.append(error) continue } let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint) if tuple.contains(where: { $0 == nil }) { continue } if !keys.contains(tuple) { issues.append(XSDFinding( .identityKeyrefResolves, reason: "keyref '\(constraint.name)': no matching key '\(refer)'", at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings), )) } } } /// The error when a field of `constraint` selects more than one node at /// `target`. A field must evaluate to a node-set with at most one member /// (Identity-constraint Definition; cvc-identity-constraint.3); selecting /// several is a violation, not a multi-valued key. private func fieldError( _ constraint: IdentityConstraint, at target: PureXML.Model.TreeNode, path targetPath: [PureXML.Validation.PathKey], ) -> XSDFinding? { let step = fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings) for field in constraint.fields { let selected = select(field, at: target, namespaces: constraint.namespaceBindings) // cvc-identity-constraint.3: each field must evaluate to at most one node. if selected.count > 1 { return XSDFinding( .identityFieldCardinality, reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must select at most one node", at: targetPath + step, ) } // cvc-identity-constraint.3 / c-fields-xpaths: a field must identify a node // with a simple type (or simple content). An element with element children // has complex content (checked structurally), and an element whose DECLARED // type is a complex type with non-simple content (empty or element-only, so // an attributes-only element with no children, XSTS idG006) is equally // invalid (checked through the schema type model). A simple-typed or // simple-content element, a text-only node, and an attribute are left alone. let element = selected.first.flatMap { $0.kind == .element ? $0 : nil } let hasElementChildren = element?.children.contains { $0.kind == .element } ?? false // A field whose DECLARED target is non-simple (the ur-type anyType or // an inline complex type without simple content, resolved at compile // time, XSTS idZ010) is invalid unless the instance gives the node a // simple type through xsi:type: cvc-identity-constraint.3 reads the // node's governing type, so a text-only anyType element validates only // when xsi:type names a simple type. With an xsi:type present the // runtime value comparison already governs, so the field is left. let declaredNonSimple = element.map { node in nonSimpleFields.contains(PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field)) && Self.xsiTypeLocalName(of: node) == nil } ?? false if element != nil, hasElementChildren || declaredNonSimple || fieldTargetIsComplexNonSimple(field, at: target) { return XSDFinding( .identityFieldMustBeSimple, reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must identify a node with simple content", at: targetPath + step, ) } } return nil } // MARK: XPath helpers private func select(_ xpath: String, at node: PureXML.Model.TreeNode, namespaces: [String: String]) -> [PureXML.Model.TreeNode] { guard let query = cache.query(xpath) else { return [] } return query.nodes(over: node, namespaces: namespaces) } private func fieldTuple(_ fields: [String], at node: PureXML.Model.TreeNode, constraint: IdentityConstraint) -> [FieldValue?] { // The field XPath's prefixes resolve against the schema (where the // constraint is declared); a QName field VALUE resolves against the // instance, so the FieldValue keeps the instance bindings. let instanceNamespaces = namespaceBindings(at: node) return fields.map { field in guard let query = cache.query(field), let value = try? query.value(at: node, namespaces: constraint.namespaceBindings) else { return nil } let type = resolveFieldType(field, constraint: constraint, at: node) // An absent attribute (empty node-set) or an empty element whose // declaration carries a `default`/`fixed` takes that value as its // identity component. A present attribute (even value "") and a // non-empty element are instance-supplied and never defaulted. if let defaulted = defaultedValue(field, value: value, type: type, constraint: constraint, at: node) { return defaulted } if let nodes = value.nodes, nodes.isEmpty { return nil } return FieldValue(string: value.string, type: type ?? Self.effectiveType(of: value), namespaceBindings: instanceNamespaces) } } /// Prefix bindings in scope at `node`, gathered from ancestor elements. func namespaceBindings(at node: PureXML.Model.TreeNode) -> [String: String] { var chain: [PureXML.Model.TreeNode] = [] var current: PureXML.Model.TreeNode? = node while let element = current, element.kind == .element { chain.append(element) current = element.parent } var bindings: [String: String] = [:] for element in chain.reversed() { for attribute in element.attributes { if attribute.name.prefix == "xmlns" { bindings[attribute.name.localName] = attribute.value } else if attribute.name.prefix == nil, attribute.name.localName == "xmlns" { bindings[""] = attribute.value } } } return bindings } /// The simple type a field value compares in: the built-in named by the /// selected node's `xsi:type`, when present. Identity-constraint values /// compare in their value space, so two `xsi:type="xsd:decimal"` fields /// `3.0` and `3` are the same value and violate a `unique`/`key`. With no /// `xsi:type` the comparison falls back to the lexical form (the type /// declared in the schema is not threaded into identity validation). private func resolveFieldType(_ field: String, constraint: IdentityConstraint, at target: PureXML.Model.TreeNode) -> SimpleType? { if field == ".", let local = target.name?.localName { if let type = fieldTypes[PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field, targetLocal: local)] { return type } guard let name = target.name else { return nil } if let declaration = types[PureXML.Schema.XSDParser.elementDeclarationKey(name)] { return simpleType(of: declaration) } if let declaration = types[PureXML.Schema.XSDParser.elementKey(name.localName)] { return simpleType(of: declaration) } } return fieldTypes[PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field)] } } }