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.
113 lines
4.1 KiB
Swift
113 lines
4.1 KiB
Swift
// The declarative combinator algebra for ``PureXML/Validation/Validation``.
|
|
// Free functions returning closures, so validations compose by value rather than
|
|
// through imperative control flow. Generic over both the subject type and the
|
|
// document, per the one-framework-parameterized-over-the-document rule.
|
|
|
|
typealias Validatable = PureXML.Validation.Validatable
|
|
typealias VContext<Subject: PureXML.Validation.Validatable, Document> =
|
|
PureXML.Validation.ValidationContext<Subject, Document>
|
|
typealias VError = PureXML.Validation.ValidationError
|
|
typealias VRule<Subject: PureXML.Validation.Validatable, Document> =
|
|
PureXML.Validation.Validation<Subject, Document>
|
|
|
|
// MARK: Context -> error array
|
|
|
|
func && <T: Validatable, D>(
|
|
lhs: @escaping (VContext<T, D>) -> [VError],
|
|
rhs: @escaping (VContext<T, D>) -> [VError],
|
|
) -> (VContext<T, D>) -> [VError] {
|
|
{ context in lhs(context) + rhs(context) }
|
|
}
|
|
|
|
func || <T: Validatable, D>(
|
|
lhs: @escaping (VContext<T, D>) -> [VError],
|
|
rhs: @escaping (VContext<T, D>) -> [VError],
|
|
) -> (VContext<T, D>) -> [VError] {
|
|
{ context in
|
|
let left = lhs(context)
|
|
if left.isEmpty { return [] }
|
|
let right = rhs(context)
|
|
return right.isEmpty ? [] : left + right
|
|
}
|
|
}
|
|
|
|
// MARK: Context -> Bool
|
|
|
|
func && <T: Validatable, D>(
|
|
lhs: @escaping (VContext<T, D>) -> Bool,
|
|
rhs: @escaping (VContext<T, D>) -> Bool,
|
|
) -> (VContext<T, D>) -> Bool {
|
|
{ context in lhs(context) && rhs(context) }
|
|
}
|
|
|
|
func || <T: Validatable, D>(
|
|
lhs: @escaping (VContext<T, D>) -> Bool,
|
|
rhs: @escaping (VContext<T, D>) -> Bool,
|
|
) -> (VContext<T, D>) -> Bool {
|
|
{ context in lhs(context) || rhs(context) }
|
|
}
|
|
|
|
// MARK: Subject KeyPath -> Bool
|
|
|
|
func == <T: Validatable, U: Equatable, D>(lhs: KeyPath<T, U>, rhs: U) -> (VContext<T, D>) -> Bool {
|
|
{ $0.subject[keyPath: lhs] == rhs }
|
|
}
|
|
|
|
func != <T: Validatable, U: Equatable, D>(lhs: KeyPath<T, U>, rhs: U) -> (VContext<T, D>) -> Bool {
|
|
{ $0.subject[keyPath: lhs] != rhs }
|
|
}
|
|
|
|
func > <T: Validatable, U: Comparable, D>(lhs: KeyPath<T, U>, rhs: U) -> (VContext<T, D>) -> Bool {
|
|
{ $0.subject[keyPath: lhs] > rhs }
|
|
}
|
|
|
|
func >= <T: Validatable, U: Comparable, D>(lhs: KeyPath<T, U>, rhs: U) -> (VContext<T, D>) -> Bool {
|
|
{ $0.subject[keyPath: lhs] >= rhs }
|
|
}
|
|
|
|
func < <T: Validatable, U: Comparable, D>(lhs: KeyPath<T, U>, rhs: U) -> (VContext<T, D>) -> Bool {
|
|
{ $0.subject[keyPath: lhs] < rhs }
|
|
}
|
|
|
|
func <= <T: Validatable, U: Comparable, D>(lhs: KeyPath<T, U>, rhs: U) -> (VContext<T, D>) -> Bool {
|
|
{ $0.subject[keyPath: lhs] <= rhs }
|
|
}
|
|
|
|
// MARK: Digging and lifting
|
|
|
|
/// Dig to a value by KeyPath and run arbitrary logic on it.
|
|
func take<T: Validatable, U, D>(_ path: KeyPath<T, U>, check: @escaping (U) -> Bool) -> (VContext<T, D>) -> Bool {
|
|
{ check($0.subject[keyPath: path]) }
|
|
}
|
|
|
|
/// Run child-typed validations against a lifted value, keeping the parent path
|
|
/// and document.
|
|
func lift<T: Validatable, U: Validatable, D>(
|
|
_ path: KeyPath<T, U>,
|
|
into validations: VRule<U, D>...,
|
|
) -> (VContext<T, D>) -> [VError] {
|
|
{ context in
|
|
validations.flatMap { $0.apply(to: context.subject[keyPath: path], at: context.codingPath, in: context.document) }
|
|
}
|
|
}
|
|
|
|
/// Unwrap an optional, erroring if nil, else run child validations.
|
|
func unwrap<T: Validatable, U: Validatable, D>(
|
|
_ path: KeyPath<T, U?>,
|
|
into validations: VRule<U, D>...,
|
|
description: String? = nil,
|
|
) -> (VContext<T, D>) -> [VError] {
|
|
{ context in
|
|
guard let subject = context.subject[keyPath: path] else {
|
|
let reason = description.map { "Tried to unwrap but found nil: \($0)" } ?? "Tried to unwrap an optional and found nil"
|
|
return [VError(reason: reason, at: context.codingPath)]
|
|
}
|
|
return validations.flatMap { $0.apply(to: subject, at: context.codingPath, in: context.document) }
|
|
}
|
|
}
|
|
|
|
/// Apply many validations to the same context (`lift` with `\.self`).
|
|
func all<T: Validatable, D>(_ validations: VRule<T, D>...) -> (VContext<T, D>) -> [VError] {
|
|
{ context in validations.flatMap { $0.apply(to: context.subject, at: context.codingPath, in: context.document) } }
|
|
}
|