Slice 1 of #92. Introduce the OpenAPIKit-idiom validation framework, replacing the imperative if-tree structural validator: - ValidationError (reason + coding path) renders "<reason> at path: root/row/cell"; ValidationErrorCollection gathers every failure and is thrown once. - Validation<Subject, Document> is the atomic, composable rule value, with the multi-error and single-error Bool inits (the latter auto-forms "Failed to satisfy: <description>"). Predicate-gated, type-directed via an erased AnyValidation that guards an optional from matching a non-optional rule. - The framework is parameterized over both Subject and Document so the structural, DTD, XSD, and Schematron layers share one machinery (Document = Void for rules that need only their subject). - Validator<Document> walks the node tree, building the coding path, with blank / default factories and fluent validating / withoutValidating. - The combinator algebra (&&, ||, comparison lifts, take, lift, unwrap, all) as free functions returning closures. The structural duplicate-attribute rule is reborn as Validation<Element, Void>. Combinators and the erased wrapper are unit-tested as pure functions per the rule. The legacy Validation.Issue stays only for the not-yet-migrated schema validators and is retired in a later slice.
92 lines
3.5 KiB
Swift
92 lines
3.5 KiB
Swift
public extension PureXML.Validation {
|
|
/// A marker for values a ``Validation`` can specialize on. Conform the model
|
|
/// types (and the containers traversed) so a `Validation<Subject, Document>`
|
|
/// can fire on every `Subject` in the tree.
|
|
protocol Validatable {}
|
|
|
|
/// One step of a validation coding path: an element name (with an optional
|
|
/// sibling index) or an attribute (`@name`). Conforms to the standard-library
|
|
/// `CodingKey`, so paths render and thread without bookkeeping.
|
|
struct PathKey: CodingKey, Equatable, Sendable {
|
|
public var stringValue: String
|
|
public var intValue: Int?
|
|
|
|
public init(stringValue: String) {
|
|
self.stringValue = stringValue
|
|
intValue = nil
|
|
}
|
|
|
|
public init?(intValue: Int) {
|
|
stringValue = String(intValue)
|
|
self.intValue = intValue
|
|
}
|
|
|
|
init(stringValue: String, intValue: Int?) {
|
|
self.stringValue = stringValue
|
|
self.intValue = intValue
|
|
}
|
|
|
|
/// An element step, optionally carrying its position among same-named
|
|
/// siblings (rendered `name[index]`).
|
|
static func element(_ name: String, index: Int? = nil) -> PathKey {
|
|
PathKey(stringValue: name, intValue: index)
|
|
}
|
|
|
|
/// An attribute step, rendered `@name`.
|
|
static func attribute(_ name: String) -> PathKey {
|
|
PathKey(stringValue: "@\(name)", intValue: nil)
|
|
}
|
|
|
|
/// Renders a path the XML way: `root/item[2]/@id`.
|
|
static func render(_ path: [PathKey]) -> String {
|
|
path.map { key in
|
|
key.intValue.map { "\(key.stringValue)[\($0)]" } ?? key.stringValue
|
|
}.joined(separator: "/")
|
|
}
|
|
}
|
|
|
|
/// The read-only bundle every check receives: the whole `Document` (the
|
|
/// cross-cutting context a rule may consult), the current `Subject` value, and
|
|
/// the coding path where it sits.
|
|
struct ValidationContext<Subject: Validatable, Document> {
|
|
public let document: Document
|
|
public let subject: Subject
|
|
public let codingPath: [PathKey]
|
|
}
|
|
|
|
/// A validation failure: a reason and the coding path where it occurred. The
|
|
/// description strips a trailing period and renders the location consistently.
|
|
struct ValidationError: Swift.Error, Equatable, Sendable, CustomStringConvertible {
|
|
public let reason: String
|
|
public let codingPath: [PathKey]
|
|
|
|
public init(reason: String, at path: [PathKey]) {
|
|
self.reason = reason
|
|
codingPath = path
|
|
}
|
|
|
|
public var description: String {
|
|
let reasonText = reason.hasSuffix(".") ? String(reason.dropLast()) : reason
|
|
let path = PathKey.render(codingPath)
|
|
return path.isEmpty ? "\(reasonText) at root of document" : "\(reasonText) at path: \(path)"
|
|
}
|
|
}
|
|
|
|
/// The one value thrown at the end of validation, holding every error found.
|
|
struct ValidationErrorCollection: Swift.Error, Equatable, Sendable, CustomStringConvertible {
|
|
public let values: [ValidationError]
|
|
|
|
public init(values: [ValidationError]) {
|
|
self.values = values
|
|
}
|
|
|
|
public var description: String {
|
|
values.map(\.description).joined(separator: "\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PureXML.Model.Node: PureXML.Validation.Validatable {}
|
|
extension PureXML.Model.Element: PureXML.Validation.Validatable {}
|
|
extension PureXML.Model.Attribute: PureXML.Validation.Validatable {}
|