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

88 lines
4.5 KiB
Swift

/// Orders the model groups reachable from a set of content models so each group
/// comes after every group it references. The compositional determinism check
/// summarizes groups in this order, so its recursive walk is always a cache hit at a
/// `<group ref>` and never recurses through a reference chain. A chain is FLAT in the
/// schema document (group definitions are siblings, not nested), so it is not bounded
/// by the parser's `maxDepth`; ordering and scanning here are iterative so neither
/// step recurses on the chain length either.
enum GroupDependencyOrder {
/// Reachable group names in dependency order (dependencies first), by an iterative
/// post-order DFS from the groups the `models` reference. A name already on the
/// current path is not re-entered, so a circular reference (illegal, rejected
/// elsewhere) terminates.
static func reachable(from models: [PureXML.Model.TreeNode], context: PureXML.Schema.XSDContext) -> [String] {
var roots: [String] = []
var seen: Set<String> = []
for model in models {
for ref in referencedGroupNames(in: model) where seen.insert(ref).inserted {
roots.append(ref)
}
}
var order: [String] = []
var visited: Set<String> = []
var onPath: Set<String> = []
var work: [(name: String, exit: Bool)] = roots.reversed().map { ($0, false) }
while let (name, exit) = work.popLast() {
if exit {
onPath.remove(name)
order.append(name)
continue
}
if visited.contains(name) {
continue
}
visited.insert(name)
onPath.insert(name)
work.append((name, true))
for ref in dependencies(of: name, context: context).reversed() where !visited.contains(ref) && !onPath.contains(ref) {
work.append((ref, false))
}
}
return order
}
/// The group names referenced within `node`, by an iterative tree walk (so scanning
/// a deep model does not itself recurse on depth). It descends compositors but stops
/// at an `<element>`, exactly as the determinism walk's `summarize` treats an element
/// as terminal: a group inside an element's inline complex type belongs to THAT type's
/// own content model (checked separately), not this one, and must not be drawn in.
private static func referencedGroupNames(in node: PureXML.Model.TreeNode) -> [String] {
var names: [String] = []
var stack = [node]
while let current = stack.popLast() {
let local = PureXML.Schema.XSDNode.localName(current)
if local == "group", let ref = PureXML.Schema.XSDNode.attribute(current, "ref") {
names.append(PureXML.Schema.XSDNode.stripPrefix(ref))
}
if local != "element" {
stack.append(contentsOf: PureXML.Schema.XSDNode.elementChildren(current))
}
}
return names
}
/// The groups `name` references. A redefined group's base-model references are added
/// ONLY when the redefinition actually self-references (`<group ref=name>` inside the
/// redefinition of `name`): that reference expands to the base model
/// (`CompWalk.redefineSelfReference`), making the base's groups part of the effective
/// content model, so they must be ordered first. When the redefinition does NOT
/// self-reference, the base is fully replaced and dead, and pulling its refs in would
/// summarise unreachable groups: surfacing a conflict in a dead group is a false
/// positive, so it is deliberately excluded. This keeps the reachable set identical
/// to the lazy walk's.
private static func dependencies(of name: String, context: PureXML.Schema.XSDContext) -> [String] {
guard let definition = context.groups[name], let model = modelChild(definition) else { return [] }
var refs = referencedGroupNames(in: model)
if refs.contains(name), context.redefinedGroups.contains(name), let base = context.baseGroups[name], let baseModel = modelChild(base) {
refs += referencedGroupNames(in: baseModel)
}
return refs
}
/// The `sequence`/`choice`/`all` model child of a group definition, if any.
private static func modelChild(_ definition: PureXML.Model.TreeNode) -> PureXML.Model.TreeNode? {
PureXML.Schema.XSDNode.elementChildren(definition)
.first { ["sequence", "choice", "all"].contains(PureXML.Schema.XSDNode.localName($0) ?? "") }
}
}