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

102 lines
5.0 KiB
Swift

extension PureXML.Schema.XSDParser {
/// The effective `xs:anyAttribute` wildcard declared under `node`, including
/// any `anyAttribute` on nested `attributeGroup` references.
static func attributeWildcard(
under node: XSDTree,
_ context: PureXML.Schema.XSDContext,
visited: Set<String> = [],
) -> PureXML.Schema.Wildcard? {
// Best-effort view for type building: a not-expressible intersection makes
// the schema invalid (flagged by SchemaWildcardExpressibility), so the type
// it would have carried is never used to validate a valid instance.
switch attributeWildcardCombination(under: node, context, visited: visited) {
case let .constraint(wildcard): wildcard
case .notExpressible: nil
}
}
/// The effective `xs:anyAttribute` wildcard under `node`, or `notExpressible`
/// when intersecting the sources yields no expressible {namespace constraint}.
static func attributeWildcardCombination(
under node: XSDTree,
_ context: PureXML.Schema.XSDContext,
visited: Set<String> = [],
) -> PureXML.Schema.WildcardCombination {
// A complex type's (or attribute group's) effective {attribute wildcard} is
// the INTERSECTION of the wildcards it draws from its own `anyAttribute` and
// from every referenced attribute group (XSD 1.0 `cos-aw-intersect`): an
// attribute is admitted only if every source admits it. (An extension's
// union with its base wildcard is handled separately, in XSDComplexContent.)
var combined: PureXML.Schema.WildcardCombination = .constraint(nil)
func merge(_ next: PureXML.Schema.Wildcard?) {
guard case let .constraint(current) = combined else { return }
combined = PureXML.Schema.Wildcard.intersection(current, next)
}
if let direct = PureXML.Schema.XSDNode.firstChild(node, named: "anyAttribute").map({ wildcard($0, context) }) {
merge(direct)
}
for child in PureXML.Schema.XSDNode.elementChildren(node) {
guard case .constraint = combined else { break }
guard PureXML.Schema.XSDNode.localName(child) == "attributeGroup",
let ref = PureXML.Schema.XSDNode.attribute(child, "ref")
else { continue }
switch referencedGroupWildcard(PureXML.Schema.XSDNode.stripPrefix(ref), context, visited: visited) {
case let .constraint(found): merge(found)
case .notExpressible: combined = .notExpressible
}
}
return combined
}
/// The effective wildcard combination of the attribute group referenced by
/// `name`, resolving a redefine self-reference against the group's base.
private static func referencedGroupWildcard(
_ name: String,
_ context: PureXML.Schema.XSDContext,
visited: Set<String>,
) -> PureXML.Schema.WildcardCombination {
if visited.contains(name) {
guard context.redefinedAttributeGroups.contains(name),
let base = context.baseAttributeGroups[name]
else { return .constraint(nil) }
return attributeWildcardCombination(under: base, context, visited: visited)
}
guard let group = context.attributeGroups[name] else { return .constraint(nil) }
let scoped = context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(group))
return attributeWildcardCombination(under: group, scoped, visited: visited.union([name]))
}
/// Parses a wildcard's `namespace` and `processContents` constraints.
static func wildcard(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.Wildcard {
let processContents: PureXML.Schema.ProcessContents = switch PureXML.Schema.XSDNode.attribute(node, "processContents") {
case "skip": .skip
case "lax": .lax
default: .strict
}
let namespace = wildcardNamespace(PureXML.Schema.XSDNode.attribute(node, "namespace"), context)
return PureXML.Schema.Wildcard(namespace: namespace, processContents: processContents, targetNamespace: context.targetNamespace)
}
private static func wildcardNamespace(_ value: String?, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.WildcardNamespace {
switch value {
case nil, "##any": return .any
case "##other":
// `##other` = any namespace name other than the target, never absent.
// With no target namespace it is simply "any non-absent namespace".
if let target = context.targetNamespace, !target.isEmpty {
return .notNamespace(target)
}
return .notAbsent
default:
let uris = (value ?? "").split(whereSeparator: \.isWhitespace).map { token -> String in
switch token {
case "##targetNamespace": context.targetNamespace ?? ""
case "##local": ""
default: String(token)
}
}
return .enumerated(Set(uris))
}
}
}