PureXML/Sources/XInclude/URIReference.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

160 lines
6.0 KiB
Swift

extension PureXML.XInclude {
/// A parsed URI reference (scheme, authority, path, query, fragment) and the
/// RFC 3986 reference-resolution algorithm. No I/O: it only manipulates the
/// identifier strings so a relative `href` can be resolved against a base.
struct URIReference: Equatable {
var scheme: String?
var authority: String?
var path: String
var query: String?
var fragment: String?
/// Resolves `reference` against `base`, returning the resolved URI string.
static func resolve(_ reference: String, against base: String) -> String {
let ref = parse(reference)
let baseRef = parse(base)
return ref.resolved(against: baseRef).recomposed()
}
/// The RFC 3986 §5.2.2 transform.
func resolved(against base: URIReference) -> URIReference {
if scheme != nil {
return URIReference(
scheme: scheme,
authority: authority,
path: Self.removeDotSegments(path),
query: query,
fragment: fragment,
)
}
var result = URIReference(scheme: base.scheme, authority: nil, path: "", query: nil, fragment: fragment)
if let authority {
result.authority = authority
result.path = Self.removeDotSegments(path)
result.query = query
return result
}
result.authority = base.authority
resolvePath(base: base, into: &result)
return result
}
private func resolvePath(base: URIReference, into result: inout URIReference) {
if path.isEmpty {
result.path = base.path
result.query = query ?? base.query
} else {
let merged = path.hasPrefix("/") ? path : Self.merge(base: base, reference: path)
result.path = Self.removeDotSegments(merged)
result.query = query
}
}
private static func merge(base: URIReference, reference: String) -> String {
if base.authority != nil, base.path.isEmpty {
return "/" + reference
}
guard let slash = base.path.lastIndex(of: "/") else { return reference }
return base.path[...slash] + reference
}
/// The RFC 3986 §5.2.4 dot-segment removal.
static func removeDotSegments(_ path: String) -> String {
var input = Substring(path)
var output = ""
while !input.isEmpty {
if input.hasPrefix("../") {
input = input.dropFirst(3)
} else if input.hasPrefix("./") {
input = input.dropFirst(2)
} else if input.hasPrefix("/./") {
input = "/" + input.dropFirst(3)
} else if input == "/." {
input = "/"
} else if input.hasPrefix("/../") {
input = "/" + input.dropFirst(4)
dropLastSegment(&output)
} else if input == "/.." {
input = "/"
dropLastSegment(&output)
} else if input == "." || input == ".." {
input = ""
} else {
moveSegment(&input, &output)
}
}
return output
}
private static func dropLastSegment(_ output: inout String) {
if let slash = output.lastIndex(of: "/") {
output = String(output[..<slash])
} else {
output = ""
}
}
private static func moveSegment(_ input: inout Substring, _ output: inout String) {
var end = input.index(after: input.startIndex)
while end < input.endIndex, input[end] != "/" {
end = input.index(after: end)
}
output += input[input.startIndex ..< end]
input = input[end...]
}
private func recomposed() -> String {
var result = ""
if let scheme {
result += scheme + ":"
}
if let authority {
result += "//" + authority
}
result += path
if let query {
result += "?" + query
}
if let fragment {
result += "#" + fragment
}
return result
}
static func parse(_ string: String) -> URIReference {
var rest = Substring(string)
var reference = URIReference(scheme: nil, authority: nil, path: "", query: nil, fragment: nil)
if let hash = rest.firstIndex(of: "#") {
reference.fragment = String(rest[rest.index(after: hash)...])
rest = rest[..<hash]
}
if let question = rest.firstIndex(of: "?") {
reference.query = String(rest[rest.index(after: question)...])
rest = rest[..<question]
}
reference.scheme = scanScheme(&rest)
if rest.hasPrefix("//") {
rest = rest.dropFirst(2)
let end = rest.firstIndex { $0 == "/" } ?? rest.endIndex
reference.authority = String(rest[..<end])
rest = rest[end...]
}
reference.path = String(rest)
return reference
}
private static func scanScheme(_ rest: inout Substring) -> String? {
guard let colon = rest.firstIndex(of: ":") else { return nil }
let candidate = rest[..<colon]
guard let first = candidate.first, first.isLetter,
candidate.allSatisfy({ $0.isLetter || $0.isNumber || $0 == "+" || $0 == "-" || $0 == "." }),
!candidate.contains("/")
else {
return nil
}
rest = rest[rest.index(after: colon)...]
return String(candidate)
}
}
}