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.
149 lines
5.5 KiB
Swift
149 lines
5.5 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) } }
|
|
}
|
|
|
|
/// Resolve a named value from the document, error if missing, else validate the resolved value.
|
|
func lookup<T: Validatable, U: Validatable, D>(
|
|
_ path: KeyPath<D, [String: U]>,
|
|
name: KeyPath<T, String>,
|
|
missing: @escaping (String) -> String,
|
|
into validations: VRule<U, D>...,
|
|
) -> (VContext<T, D>) -> [VError] {
|
|
{ context in
|
|
let key = context.subject[keyPath: name]
|
|
guard let resolved = context.document[keyPath: path][key] else {
|
|
return [VError(reason: missing(key), at: context.codingPath)]
|
|
}
|
|
return validations.flatMap { $0.apply(to: resolved, at: context.codingPath, in: context.document) }
|
|
}
|
|
}
|
|
|
|
/// Resolve an optional named reference from the document: a nil reference is
|
|
/// not an error, a named-but-unresolved reference errors, else validate the
|
|
/// resolved value.
|
|
func unwrapAndLookup<T: Validatable, U: Validatable, D>(
|
|
_ path: KeyPath<D, [String: U]>,
|
|
name: KeyPath<T, String?>,
|
|
missing: @escaping (String) -> String,
|
|
into validations: VRule<U, D>...,
|
|
) -> (VContext<T, D>) -> [VError] {
|
|
{ context in
|
|
guard let key = context.subject[keyPath: name] else { return [] }
|
|
guard let resolved = context.document[keyPath: path][key] else {
|
|
return [VError(reason: missing(key), at: context.codingPath)]
|
|
}
|
|
return validations.flatMap { $0.apply(to: resolved, at: context.codingPath, in: context.document) }
|
|
}
|
|
}
|