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

58 lines
2.6 KiB
Swift

extension PureXML.Schema.Particle {
/// Eliminates pointless particles so the restriction algorithm sees the spec's
/// normalized content model: a `maxOccurs=0` member contributes nothing and is
/// dropped; a nested same-compositor `{1,1}` group is spliced into its parent;
/// a `{1,1}` group with a single member is unwrapped to that member.
/// Language-preserving.
func normalized() -> Self {
guard case let .group(group) = term else { return self }
var members: [Self] = []
for child in group.particles {
let normalized = child.normalized()
if normalized.maxOccurs == 0 {
continue
}
let splice = normalized.minOccurs == 1 && normalized.maxOccurs == 1
if case let .group(inner) = normalized.term, inner.compositor == group.compositor, splice {
members.append(contentsOf: inner.particles)
} else {
members.append(normalized)
}
}
if members.count == 1, minOccurs == 1, maxOccurs == 1 {
return members[0]
}
return Self(minOccurs: minOccurs, maxOccurs: maxOccurs, term: .group(.init(compositor: group.compositor, particles: members)))
}
/// The minimum number of elements this particle can contribute (effective total
/// range, lower bound): a leaf is its `minOccurs`; a sequence/all sums its
/// members, a choice takes the smallest; times the particle's own `minOccurs`.
func effectiveOccurrenceMin() -> Int {
switch term {
case .element, .wildcard:
return minOccurs
case let .group(group):
let inner: Int = switch group.compositor {
case .choice: group.particles.map { $0.effectiveOccurrenceMin() }.min() ?? 0
case .sequence, .all: group.particles.map { $0.effectiveOccurrenceMin() }.reduce(0, +)
}
return minOccurs * inner
}
}
/// The maximum number of elements this particle can contribute (nil = unbounded).
func effectiveOccurrenceMax() -> Int? {
switch term {
case .element, .wildcard:
return maxOccurs
case let .group(group):
let childMaxes = group.particles.map { $0.effectiveOccurrenceMax() }
guard let particleMax = maxOccurs, !childMaxes.contains(where: { $0 == nil }) else { return nil }
let maxes = childMaxes.compactMap(\.self)
let inner = group.compositor == .choice ? (maxes.max() ?? 0) : maxes.reduce(0, +)
return particleMax * inner
}
}
}