PureXML/Sources/Parsing/PseudoAttributeScanner.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

80 lines
2.8 KiB
Swift

extension PureXML.Parsing.XMLDeclaration {
/// A name/value pair from an XML declaration.
struct PseudoAttribute: Equatable {
let name: String
let value: String
}
/// Scans the `name="value"` pseudo-attributes of an XML declaration's text.
/// Each value must be quoted; anything malformed (a missing `=`, an unquoted
/// value, trailing junk) makes the whole scan fail so the declaration is
/// rejected rather than partly read.
struct PseudoAttributeScanner {
private let chars: [Character]
private var index = 0
init(_ text: String) {
chars = Array(text)
}
mutating func scan() -> [PseudoAttribute]? {
var pairs: [PseudoAttribute] = []
skipSpace()
while index < chars.count {
guard let name = scanName(), !name.isEmpty else { return nil }
skipSpace()
guard consume("=") else { return nil }
skipSpace()
guard let value = scanQuoted() else { return nil }
pairs.append(PseudoAttribute(name: name, value: value))
// Pseudo-attributes must be separated by whitespace.
let before = index
skipSpace()
if index == before, index < chars.count {
return nil
}
}
return pairs
}
private mutating func scanName() -> String? {
var name = ""
while index < chars.count, isNameCharacter(chars[index]) {
name.append(chars[index])
index += 1
}
return name.isEmpty ? nil : name
}
private mutating func scanQuoted() -> String? {
guard index < chars.count, chars[index] == "\"" || chars[index] == "'" else { return nil }
let quote = chars[index]
index += 1
var value = ""
while index < chars.count, chars[index] != quote {
value.append(chars[index])
index += 1
}
guard index < chars.count else { return nil }
index += 1
return value
}
private mutating func consume(_ character: Character) -> Bool {
guard index < chars.count, chars[index] == character else { return false }
index += 1
return true
}
private mutating func skipSpace() {
while index < chars.count, chars[index] == " " || chars[index] == "\t" || chars[index] == "\n" || chars[index] == "\r" {
index += 1
}
}
private func isNameCharacter(_ character: Character) -> Bool {
character.isLetter || character.isNumber || character == "-" || character == "_" || character == "."
}
}
}