PureXML/Sources/Schema/SchemaNotationValidity.swift
Mihaela Mihaljevic 380160cc2b refactor(validation): decompose XSD instance validation into composable rules
The two monolithic XSD content/identity rules only satisfied the OpenAPIKit
validation idiom at the wrong granularity: no per-constraint removability,
no isolable failing/near-miss tests. Split them into 32 named rules, each
finding tagged at its origin and cached per XSDContext so the walk still
runs once per document (the #101 decision against duplicating XSD's
recursive type resolution still holds). Wired the streaming path's ID/IDREF
and identity checks through the same framework instead of calling the
validators imperatively, and added the unwrapAndLookup combinator the rule
already named as required.

Also includes the mandated repo-wide swiftformat/swiftlint pass.
2026-07-12 14:12:17 +02:00

130 lines
6.2 KiB
Swift

extension PureXML.Schema.XSDParser {
static func notationValidityFindings(
_ containers: [XSDTree],
_ context: PureXML.Schema.XSDContext,
) -> [PureXML.Schema.SchemaLocatedFinding] {
let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: context.targetNamespace)
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
var declaredNotations: Set<PureXML.Model.QualifiedName> = []
// 1. Collect all declared notations and validate notation declarations
for index in containers.indices {
let container = containers[index]
let containerNamespace = namespaceMap[index] ?? context.targetNamespace
for child in PureXML.Schema.XSDNode.elementChildren(container) {
guard child.name?.namespaceURI == xsdNamespace,
PureXML.Schema.XSDNode.localName(child) == "notation"
else {
continue
}
let name = PureXML.Schema.XSDNode.attribute(child, "name") ?? ""
if PureXML.Schema.XSDNode.attribute(child, "public") == nil, PureXML.Schema.XSDNode.attribute(child, "system") == nil {
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "notation '\(name)' must specify at least one of public or system attributes",
node: child,
))
}
if notationHasForbiddenContent(child) {
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "notation '\(name)' may contain only an optional annotation, no other element or character content",
node: child,
))
}
let qName = PureXML.Model.QualifiedName(localName: name, namespaceURI: containerNamespace)
declaredNotations.insert(qName)
}
}
// 2. Validate notation enumeration constraints on simpleTypes
for index in containers.indices {
let container = containers[index]
walkSimpleTypes(container, context: context, declaredNotations: declaredNotations, into: &findings)
}
return findings
}
/// Whether a `notation` declaration carries content its `(annotation?)` model
/// forbids: any element child other than an `xsd:annotation`, or any
/// non-whitespace character content. Whitespace and a single optional
/// annotation are allowed.
private static func notationHasForbiddenContent(_ node: XSDTree) -> Bool {
let hasForeignElement = PureXML.Schema.XSDNode.elementChildren(node).contains {
$0.name?.namespaceURI != xsdNamespace || PureXML.Schema.XSDNode.localName($0) != "annotation"
}
let hasText = node.children.contains {
($0.kind == .text || $0.kind == .cdata) && $0.value.contains { !$0.isWhitespace }
}
return hasForeignElement || hasText
}
private static func walkSimpleTypes(
_ node: XSDTree,
context: PureXML.Schema.XSDContext,
declaredNotations: Set<PureXML.Model.QualifiedName>,
into findings: inout [PureXML.Schema.SchemaLocatedFinding],
) {
let local = PureXML.Schema.XSDNode.localName(node)
if local == "appinfo" || local == "documentation" {
return
}
if node.name?.namespaceURI == xsdNamespace, local == "restriction" {
validateNotationRestriction(node, context: context, declaredNotations: declaredNotations, into: &findings)
}
for child in PureXML.Schema.XSDNode.elementChildren(node) {
walkSimpleTypes(child, context: context, declaredNotations: declaredNotations, into: &findings)
}
}
private static func validateNotationRestriction(
_ node: XSDTree,
context: PureXML.Schema.XSDContext,
declaredNotations: Set<PureXML.Model.QualifiedName>,
into findings: inout [PureXML.Schema.SchemaLocatedFinding],
) {
guard let parent = node.parent,
parent.name?.namespaceURI == xsdNamespace,
PureXML.Schema.XSDNode.localName(parent) == "simpleType",
let baseAttr = PureXML.Schema.XSDNode.attribute(node, "base")
else {
return
}
let baseType = PureXML.Schema.XSDSimpleParser.simpleTypeReference(baseAttr, context)
guard baseType.base == PureXML.Schema.BuiltinType.notation else { return }
let bindings = collectBindings(from: node)
for child in PureXML.Schema.XSDNode.elementChildren(node) where child.name?.namespaceURI == xsdNamespace && PureXML.Schema.XSDNode.localName(child) == "enumeration" {
guard let value = PureXML.Schema.XSDNode.attribute(child, "value") else { continue }
let prefix = PureXML.Schema.XSDNode.prefix(value)
let resolvedNamespace = prefix == "xml" ? "http://www.w3.org/XML/1998/namespace" : PureXML.Schema.XSDNode.referenceNamespace(value, bindings)
let localVal = PureXML.Schema.XSDNode.stripPrefix(value)
let valQName = PureXML.Model.QualifiedName(prefix: prefix, localName: localVal, namespaceURI: resolvedNamespace)
let isDeclared = declaredNotations.contains { dec in
let decNS = (dec.namespaceURI == "") ? nil : dec.namespaceURI
let valNS = (valQName.namespaceURI == "") ? nil : valQName.namespaceURI
return dec.localName == valQName.localName && decNS == valNS
}
if !isDeclared {
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "notation enumeration value '\(value)' does not name a declared notation",
node: child,
))
}
}
}
private static func collectBindings(from node: XSDTree) -> [String: String] {
var bindings: [String: String] = [:]
var current: XSDTree? = node
while let element = current {
for (prefix, uri) in PureXML.Schema.XSDNode.namespaceBindings(of: element) where bindings[prefix] == nil {
bindings[prefix] = uri
}
current = element.parent
}
return bindings
}
}