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

167 lines
6.6 KiB
Swift

/// The prohibition flags one path walk carries (7.1).
struct RelaxNGPathFlags {
var inAttribute = false
var inOneOrMoreGroupOrInterleave = false
var underOneOrMore = false
var inList = false
}
/// The per-element walks: prohibited paths (7.1), with refs expanding
/// transparently and elements as the simplified grammar's ref targets.
extension RelaxNGRestrictionChecker {
// MARK: Per-element checks (7.1-7.4, 4.16)
/// Walks every element pattern reachable from `start`, checking its
/// content against the path, content-type, attribute, and interleave
/// restrictions.
func elementProblems(_ start: Pattern) -> String? {
var queue: [Pattern] = [start]
var enqueuedDefines: Set<String> = []
var seenElements = 0
while let pattern = queue.popLast() {
switch pattern {
case let .element(nameClass, content):
seenElements += 1
if seenElements > 10000 {
return nil
}
_ = nameClass
if let problem = contentProblem(content) {
return problem
}
queue.append(content)
case let .attribute(_, content):
queue.append(content)
case let .choice(lhs, rhs), let .group(lhs, rhs), let .interleave(lhs, rhs):
queue.append(lhs)
queue.append(rhs)
case let .oneOrMore(inner), let .list(inner), let .dataExcept(_, inner):
queue.append(inner)
case let .ref(name):
if enqueuedDefines.insert(name).inserted {
queue.append(resolve(name))
}
default:
break
}
}
return nil
}
/// One element's content: prohibited paths under attribute, oneOrMore
/// group/interleave, and list (7.1), a computable content type (7.2),
/// distinct attributes (7.3), and interleave constraints (7.4).
func contentProblem(_ content: Pattern) -> String? {
if let problem = pathProblem(content, flags: RelaxNGPathFlags(), visiting: []) {
return problem
}
if contentType(content, visiting: []) == nil {
return "element content does not have a computable content type (7.2)"
}
if let problem = attributeSets(content, visiting: []).problem {
return problem
}
return interleaveProblem(content, visiting: [])
}
func pathProblem(_ pattern: Pattern, flags: RelaxNGPathFlags, visiting: Set<String>) -> String? {
switch pattern {
case let .attribute(nameClass, content):
return attributePathProblem(nameClass, content, flags: flags, visiting: visiting)
case let .ref(name):
guard !visiting.contains(name) else { return nil }
return pathProblem(resolve(name), flags: flags, visiting: visiting.union([name]))
case .text:
return flags.inList ? "text is not allowed inside list (7.1.3)" : nil
case .element:
return elementPathProblem(flags)
default:
return compositePathProblem(pattern, flags: flags, visiting: visiting)
}
}
/// The composite cases: list opens the 7.1.3 context, group and
/// interleave under oneOrMore open the 7.1.2 context.
private func compositePathProblem(_ pattern: Pattern, flags: RelaxNGPathFlags, visiting: Set<String>) -> String? {
switch pattern {
case let .list(inner):
if flags.inList {
return "list is not allowed inside list (7.1.3)"
}
var nested = flags
nested.inList = true
return pathProblem(inner, flags: nested, visiting: visiting)
case let .interleave(lhs, rhs), let .group(lhs, rhs):
if flags.inList, isInterleave(pattern) {
return "interleave is not allowed inside list (7.1.3)"
}
var nested = flags
if flags.underOneOrMore {
nested.inOneOrMoreGroupOrInterleave = true
}
return pathProblem(lhs, flags: nested, visiting: visiting) ?? pathProblem(rhs, flags: nested, visiting: visiting)
case let .choice(lhs, rhs):
return pathProblem(lhs, flags: flags, visiting: visiting) ?? pathProblem(rhs, flags: flags, visiting: visiting)
case let .oneOrMore(inner):
var nested = flags
nested.underOneOrMore = true
return pathProblem(inner, flags: nested, visiting: visiting)
case let .dataExcept(_, inner):
return pathProblem(inner, flags: flags, visiting: visiting)
default:
return nil
}
}
private func isInterleave(_ pattern: Pattern) -> Bool {
if case .interleave = pattern {
return true
}
return false
}
/// The attribute prohibitions of 7.1.1-7.1.3 and the infinite-name-class
/// rule of 7.3, then the attribute's own content under the inner flag.
private func attributePathProblem(_ nameClass: NameClass, _ content: Pattern, flags: RelaxNGPathFlags, visiting: Set<String>) -> String? {
if flags.inAttribute {
return "attribute is not allowed inside attribute (7.1.1)"
}
if flags.inOneOrMoreGroupOrInterleave {
return "attribute is not allowed under oneOrMore with group or interleave (7.1.2)"
}
if flags.inList {
return "attribute is not allowed inside list (7.1.3)"
}
if !flags.underOneOrMore, isInfinite(nameClass) {
return "an attribute with an infinite name class must be inside oneOrMore (7.3)"
}
var inner = flags
inner.inAttribute = true
return pathProblem(content, flags: inner, visiting: visiting)
}
/// In the simplified grammar a ref stands for an element, so the
/// prohibitions on ref under attribute and list translate to the element.
private func elementPathProblem(_ flags: RelaxNGPathFlags) -> String? {
if flags.inAttribute {
return "element is not allowed inside attribute (7.1.1)"
}
if flags.inList {
return "element is not allowed inside list (7.1.3)"
}
return nil // Otherwise a new context, walked separately.
}
/// Whether a name class admits infinitely many names.
func isInfinite(_ nameClass: NameClass) -> Bool {
switch nameClass {
case .anyName, .anyNameExcept, .nsName, .nsNameExcept:
true
case let .choice(lhs, rhs):
isInfinite(lhs) || isInfinite(rhs)
case .name:
false
}
}
}