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

70 lines
3.7 KiB
Swift

extension PureXML.Schema.XSDParser {
/// The lookups a simple-type variety walk needs, bundled so the walk's base resolution
/// is O(1) (the name map) and threadable without a long parameter list.
struct VarietyLookup {
let byName: [String: XSDTree]
let bindings: [String: String]
let target: String?
/// Built from a schema element: its global `simpleType` definitions keyed by name
/// (first declaration wins, matching the prior first-match scan), so the variety
/// walk looks a base up in O(1) instead of re-scanning the schema's children at
/// every link of a `restriction` chain (the former O(n^3) on a long, hostile chain).
init(schema: XSDTree, bindings: [String: String], target: String?) {
var byName: [String: XSDTree] = [:]
for child in PureXML.Schema.XSDNode.elementChildren(schema) where PureXML.Schema.XSDNode.localName(child) == "simpleType" {
if let name = PureXML.Schema.XSDNode.attribute(child, "name"), byName[name] == nil {
byName[name] = child
}
}
self.byName = byName
self.bindings = bindings
self.target = target
}
}
/// The variety (`"list"`, `"union"`, or nil for atomic/unknown) a `restriction`
/// derives from: an inline `simpleType` base wins, otherwise the `base` QName.
/// `cache` memoizes each named type's variety, so a chain is walked once, not per type.
static func restrictionBaseVariety(_ restriction: XSDTree, _ lookup: VarietyLookup, _ visited: Set<String>, _ cache: inout [String: String?]) -> String? {
if let inlineBase = PureXML.Schema.XSDNode.firstChild(restriction, named: "simpleType") {
return simpleTypeVariety(inlineBase, lookup, visited, &cache)
}
guard let base = PureXML.Schema.XSDNode.attribute(restriction, "base") else { return nil }
return namedTypeVariety(base, lookup, visited, &cache)
}
private static func simpleTypeVariety(_ simpleType: XSDTree, _ lookup: VarietyLookup, _ visited: Set<String>, _ cache: inout [String: String?]) -> String? {
if PureXML.Schema.XSDNode.firstChild(simpleType, named: "list") != nil {
return "list"
}
if PureXML.Schema.XSDNode.firstChild(simpleType, named: "union") != nil {
return "union"
}
if let restriction = PureXML.Schema.XSDNode.firstChild(simpleType, named: "restriction") {
return restrictionBaseVariety(restriction, lookup, visited, &cache)
}
return nil
}
/// The variety of a named base, resolved namespace-aware so a same-local-name
/// collision never flips the variety: a base in the XSD namespace is a built-in
/// (only the list datatypes are list-variety; the rest atomic, nil); a base in
/// the schema's own target namespace resolves to a declared global `simpleType`
/// (cycle-guarded); a base in any other namespace is external and skipped (nil).
private static func namedTypeVariety(_ base: String, _ lookup: VarietyLookup, _ visited: Set<String>, _ cache: inout [String: String?]) -> String? {
let (prefix, local) = splitQName(base)
let uri = prefix.map { lookup.bindings[$0] } ?? lookup.bindings[""]
if uri == xsdNamespace {
return builtinListTypes.contains(local) ? "list" : nil
}
if let cached = cache[local] {
return cached
}
guard uri == lookup.target, !visited.contains(local), let declared = lookup.byName[local] else { return nil }
let variety = simpleTypeVariety(declared, lookup, visited.union([local]), &cache)
cache[local] = variety
return variety
}
}