I'm trying to implement a generic validator in Swift. The validator simply performs some validation, i.e. email validation, and returns true
or false
based on whether or not the value is valid.
protocol Validator {
associatedtype Value
func validate(_ value: Value) -> Bool
}
I've also managed to get these validators implemented. As you can see, the associatedValue
works like a charm.
struct EmailValidator: Validator {
func validate(_ value: String) -> Bool {
NSPredicate(format: "SELF MATCHES %@", "[A-Z0-9a-z._% -] @[A-Za-z0-9.-] \\.[A-Za-z]{2,}").evaluate(with: value)
}
}
struct RequiredValidator: Validator {
func validate(_ value: String) -> Bool {
!value.isEmpty
}
}
But now I'd like to have a compound validator, that doesn't have to conform Validator
, but it does need to accept any Validator
where the associatedValue
is the same. Below I have an example.
let compoundValidator = CompoundValidator<String>(EmailValidator(), RequiredValidator())
However, when I try to implement this idea, I run into errors such as Cannot specialize non-generic type 'Validator'
or Cannot convert value of type 'Value' to expected argument type 'Validator.Value'
or Protocol 'Validator' can only be used as a generic constraint because it has Self or associated type requirements
depending on what I try to implement. Is there a way I can achieve this compound validator using generics?
struct CompoundValidator<Value> {
let validators: [Validator<Value>]
init(_ validators: Validator<Value>...) {
self.validators = validators
}
func validate(_ value: Value) -> Bool {
validators.reduce(into: true) { partialResult, validator in
partialResult = partialResult && validator.validate(value)
}
}
}
CodePudding user response:
Let's start with the beginning, Validator<Value>
is invalid syntax, even if Validator
is a protocol with associated types. Swift doesn't yet allow existential containers (protocol references) to declare generic constraints, the exception being the where
clause.
Thus, you'll need a type eraser to be able to use any kind of generics:
struct AnyValidator<Value>: Validator {
private let _validate: (Value) -> Bool
init<V: Validator>(_ validator: V) where V.Value == Value {
_validate = validator.validate
}
func validate(_ value: Value) -> Bool {
_validate(value)
}
}
extension Validator {
func eraseToAnyValidator() -> AnyValidator<Value> {
AnyValidator(self)
}
}
, and you'll need to update the definition as such:
struct CompoundValidator<Value> {
let validators: [AnyValidator<Value>]
init(_ validators: AnyValidator<Value>...) {
self.validators = validators
}
However the above implementation requires all callers to call .eraseToAnyValidator()
:
let compoundValidator = CompoundValidator(EmailValidator().eraseToAnyValidator(), RequiredValidator().eraseToAnyValidator())
If you want to decrease the burden on the caller, then you'll need do some work on the callee side:
struct CompoundValidator<Value> {
let validators: [AnyValidator<Value>]
init<V: Validator>(_ v: V) where V.Value == Value {
validators = [v.eraseToAnyValidator()]
}
init<V1: Validator, V2: Validator>(_ v1: V1, _ v2: V2) where V1.Value == Value, V2.Value == Value {
validators = [v1.eraseToAnyValidator(), v2.eraseToAnyValidator()]
}
init<V1: Validator, V2: Validator, V3: Validator>(_ v1: V1, _ v2: V2, _ v3: V3) where V1.Value == Value, V2.Value == Value, V3.Value == Value {
validators = [v1.eraseToAnyValidator(), v2.eraseToAnyValidator(), v3.eraseToAnyValidator()]
}
, basically you'll need to add as many initializers as many arguments you'll want to pass through the codebase. This enables you to call the initializer in the simple form:
let compoundValidator = CompoundValidator(EmailValidator(), RequiredValidator())
CodePudding user response:
struct CompoundValidator<Value, T: Validator> where T.Value == Value {
let validators: [T]
init(_ validators: T...) {
self.validators = validators
}
func validate(_ value: Value) -> Bool {
validators.reduce(into: true) { partialResult, validator in
partialResult = partialResult && validator.validate(value)
}
}
}
CodePudding user response:
You can create an AnyValidator
type, that is a concrete type. This way you can use it to specify the generic type arguments.
struct AnyValidator<Value>: Validator {
private let validateFunc: (Value) -> Bool
init<T: Validator>(_ validator: T) where T.Value == Value {
validateFunc = validator.validate
}
func validate(_ value: Value) -> Bool {
validateFunc(value)
}
}
extension Validator {
func eraseToAnyValidator() -> AnyValidator<Value> {
.init(self)
}
}
struct CompoundValidator<Value> {
let validators: [AnyValidator<Value>]
init(_ validators: AnyValidator<Value>...) {
self.validators = validators
}
func validate(_ value: Value) -> Bool {
validators.reduce(into: true) { partialResult, validator in
partialResult = partialResult && validator.validate(value)
}
}
}
Example usage:
CompoundValidator(
EmailValidator().eraseToAnyValidator(),
RequiredValidator().eraseToAnyValidator())
You might want to provide convenience initialisers for few numbers of validators:
init<V1, V2>(_ v1: V1, _ v2: V2) where V1: Validator, V2: Validator, V1.Value == Value, V2.Value == Value {
self.init(v1.eraseToAnyValidator(), v2.eraseToAnyValidator())
}
init<V1, V2, V3>(_ v1: V1, _ v2: V2, _ v3: V3) where V1: Validator, V2: Validator, V3: Validator, V1.Value == Value, V2.Value == Value, V3.Value == Value {
self.init(v1.eraseToAnyValidator(), v2.eraseToAnyValidator(), v3.eraseToAnyValidator())
}
which allows you to omit .eraseToAnyValidator()
.
This is similar to how Combine's Publisher
type has a merge
operator that accepts up to eight arguments, just to allow you to merge different types of Publisher
s without saying eraseToAnyPublisher
.