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

129 lines
4.2 KiB
Swift

/// Name-class and token-stream helpers for ``RNCParser``. Kept in a separate
/// file so the parser proper stays within the type/file length budget.
extension RNCParser {
// MARK: Name classes
func parseNameClass(attribute: Bool) -> NameClass {
var nameClass = parseSimpleNameClass(attribute: attribute)
while peek() == .symbol("|") {
advance()
nameClass = .choice(nameClass, parseSimpleNameClass(attribute: attribute))
}
return nameClass
}
private func parseSimpleNameClass(attribute: Bool) -> NameClass {
let base = parseNameClassPrimary(attribute: attribute)
// `*` and `ns:*` may be narrowed by `- nameClass` subtraction.
guard peek() == .symbol("-") else { return base }
advance()
return subtract(base, parseSimpleNameClass(attribute: attribute))
}
private func parseNameClassPrimary(attribute: Bool) -> NameClass {
switch peek() {
case .symbol("*"):
advance()
return .anyName
case .symbol("("):
advance()
let nameClass = parseNameClass(attribute: attribute)
expectSymbol(")")
return nameClass
// `prefix:*` lexes as the word `prefix:` (a `:` is a name character)
// followed by a `*` symbol: a namespace wildcard.
case let .word(word) where word.hasSuffix(":") && peek(1) == .symbol("*"):
advance()
advance()
return .nsName(namespaces[String(word.dropLast())] ?? "")
case let .word(word):
advance()
return qualifiedName(word, attribute: attribute)
default:
advance()
return .anyName
}
}
/// Applies name-class subtraction, which RELAX NG defines only for the open
/// classes `anyName` (`*`) and `nsName` (`prefix:*`).
private func subtract(_ base: NameClass, _ except: NameClass) -> NameClass {
switch base {
case .anyName: .anyNameExcept(except)
case let .nsName(namespace): .nsNameExcept(namespace: namespace, except: except)
default: base
}
}
private func qualifiedName(_ word: String, attribute: Bool) -> NameClass {
let parts = word.split(separator: ":", maxSplits: 1).map(String.init)
if parts.count == 2 {
if parts[1] == "*" {
return .nsName(namespaces[parts[0]] ?? "")
}
return .name(namespace: namespaces[parts[0]] ?? "", localName: parts[1])
}
return .name(namespace: attribute ? "" : defaultNamespace, localName: word)
}
// MARK: Token helpers
func peek(_ offset: Int = 0) -> PureXML.Schema.RNCToken? {
let index = position + offset
return index < tokens.count ? tokens[index] : nil
}
@discardableResult
func advance() -> PureXML.Schema.RNCToken? {
defer { position += 1 }
return peek()
}
func wordValue() -> String {
if case let .word(word) = peek() {
advance()
return word
}
return ""
}
func expectAssignment() -> String {
if case let .symbol(symbol) = peek(), symbol == "=" || symbol == "|=" || symbol == "&=" {
advance()
return symbol
}
return "="
}
func expectSymbol(_ symbol: String) {
if peek() == .symbol(symbol) {
advance()
}
}
func expectString() -> String {
if case let .string(value) = peek() {
advance()
return concatenated(value)
}
return ""
}
/// Folds RNC string concatenation: `"a" ~ "b"` is the single literal `"ab"`,
/// the way long strings are split across lines. The leading string has been
/// consumed; this appends each following `~ "…"`.
func concatenated(_ value: String) -> String {
var result = value
while peek() == .symbol("~"), case let .string(next)? = peek(1) {
advance()
advance()
result += next
}
return result
}
func strip(_ qualified: String) -> String {
qualified.split(separator: ":").last.map(String.init) ?? qualified
}
}