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.
88 lines
4.5 KiB
Swift
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) ?? "") }
|
|
}
|
|
}
|