Основная причина, по которой вы испытываете двусмысленность, заключается в том, что сообщения об ошибках слишком рано «появляются» Для операции &&
сообщение об ошибке не известно до тех пор, пока выражение не будет оценено.
Следовательно, не следует хранить свойство error
. Вместо этого сообщение об ошибке выводится только при возврате matches
, то есть в качестве возвращаемого значения. Конечно, вам также необходимо обработать состояние успеха, когда сообщение об ошибке отсутствует.
Swift предоставляет множество способов смоделировать это - вы можете вернуть String?
, представляющее сообщение об ошибке, или Result<(), ValidationError>
или даже Result<Target, ValidationError>
.
И если вы сделали сообщение об ошибке, возвращающее значение matches
(какой бы тип вы ни выбрали), у вас не должно быть этой проблемы неоднозначности.
Здесь я сделал это с Result<(), ValidationError>
. Честно говоря, сам код я довольно прост:
public struct ValidationError: Error {
let message: String
}
public struct Predicate<Target> {
var matches: (Target) -> Result<(), ValidationError>
// MARK: Factory methods
static func required<T: Collection>() -> Predicate<T> {
.init { !$0.isEmpty ? .success(()) : .failure(ValidationError(message: "Required field")) }
}
static func characterCountMoreThan<T: StringProtocol>(count: Int) -> Predicate<T> {
.init { $0.count > count ? .success(()) : .failure(ValidationError(message: "Length must be more than \(count) characters")) }
}
static func characterCountLessThan<T: StringProtocol>(count: Int) -> Predicate<T> {
.init { $0.count < count ? .success(()) : .failure(ValidationError(message: "Length must be less than \(count) characters")) }
}
static func characterCountWithin<T: StringProtocol>(range: Range<Int>) -> Predicate<T> {
.init {
($0.count >= range.lowerBound) && ($0.count <= range.upperBound) ?
.success(()) :
.failure(ValidationError(message: "Length must be between \(range.lowerBound) and \(range.upperBound) characters")) }
}
}
func ==<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
Predicate {
$0[keyPath: lhs] == rhs ?
.success(()) :
.failure(ValidationError(message: "Must equal \(rhs)"))
}
}
// r.g. let uncompletedItems = list.items(matching: !\.isCompleted)
prefix func !<T>(rhs: KeyPath<T, Bool>) -> Predicate<T> {
rhs == false
}
func ><T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
Predicate {
$0[keyPath: lhs] > rhs ?
.success(()) :
.failure(ValidationError(message: "Must be greater than \(rhs)"))
}
}
func <<T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
Predicate {
$0[keyPath: lhs] < rhs ?
.success(()) :
.failure(ValidationError(message: "Must be less than \(rhs)"))
}
}
func ||<T>(lhs: Predicate<T>, rhs: Predicate<T>) -> Predicate<T> {
// short-circuiting version, needs a nested switch
// Predicate {
// target in
// switch lhs.matches(target) {
// case .success:
// return .success(())
// case .failure(let leftError):
// switch rhs.matches(target) {
// case .success:
// return .success(())
// case .failure(let rightError):
// return .failure(ValidationError(message: "\(leftError.message) AND \(rightError.message)"))
// }
// }
// }
// without a nested switch, not short-circuiting
Predicate {
target in
switch (lhs.matches(target), rhs.matches(target)) {
case (.success, .success), (.success, .failure), (.failure, .success):
return .success(())
case (.failure(let leftError), .failure(let rightError)):
return .failure(ValidationError(message: "\(leftError.message) AND \(rightError.message)"))
}
}
}
func &&<T>(lhs: Predicate<T>, rhs: Predicate<T>) -> Predicate<T> {
Predicate {
target in
switch (lhs.matches(target), rhs.matches(target)) {
case (.success, .success):
return .success(())
case (.success, let rightFail):
return rightFail
case (let leftFail, .success):
return leftFail
case (.failure(let leftError), .failure(let rightError)):
return .failure(ValidationError(message: "\(leftError.message) AND \(rightError.message)"))
}
}
}
@propertyWrapper
public class ValidateAndPublishOnMain<ValueType> where ValueType: LosslessStringConvertible { // Type constraint specifically for SwiftUI text controls
@Published private var value: ValueType
private var validator: Predicate<ValueType>
public var wrappedValue: ValueType {
get { value }
set { value = newValue }
}
// need to also force validation to execute when the textfield loses focus
public var projectedValue: AnyPublisher<Result<ValueType, ValidationError>, Never> {
return $value
.receive(on: DispatchQueue.main)
.map { value in
// mapped the Result' Success type
self.validator.matches(value).map { _ in value }
}
.eraseToAnyPublisher()
}
public init(wrappedValue initialValue: ValueType, predicate: Predicate<ValueType>) {
self.value = initialValue
self.validator = predicate
}
}
Обратите внимание, что я изменил ваш ValidationError
на struct, а не на enum. Вы можете сделать это в соответствии с ExpressibleByStringLiteral
, если вам не нравится многословие ValidationError(message: ...)
.
Еще одна вещь, которую вы можете рассмотреть, - это сообщения для предикатов, включающие ключевые пути. Ключевые пути не имеют удобочитаемого строкового представления, поэтому в качестве сообщения для \.isCompleted == false
.
нельзя указывать «isCompleted must равно false».