If you haven’t read my post on Composite Validators, take a look at that first, as this is going to build off of what was created in that post.
Returning multiple errors in validators
In order to give users the best feedback when validating their input, it may be useful to know everything that’s wrong with what they entered, not just the first error encountered. Because of this, the implementation we created before needs to be modified to return multiple errors if the input is invalid.
So what does that look like?
We just have to make some relatively small changes in a few places.
Validator Protocol
We just need to change the invalid case to return an array of errors:
1
2
3
4
enum ValidatorResult {
case valid
case invalid(errors: [Error])
}
Individual Validators
These are all still going to return a single error, but they just need to do it as an array of one error. For Example:
1
2
3
4
5
6
7
8
9
10
struct PasswordLengthValidator: Validator {
func validate(_ value: String) -> ValidatorResult {
if value.characters.count >= 8 {
return .valid
} else {
return .invalid(error: [PasswordValidatorError.tooShort])
}
}
}
Composite Validator
This one is essentially just packaging things up a little differently:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct CompositeValidator: Validator {
private let validators: [Validator]
init(validators: Validator...) {
self.validators = validators
}
func validate(_ value: String) -> ValidatorResult {
return validators.reduce(.valid) { validatorResult, validator in
switch validator.validate(value) {
case .valid:
return validatorResult
case .invalid(let validatorErrors):
switch validatorResult {
case .valid:
return .invalid(errors: validatorErrors)
case .invalid(let validatorResultErrors):
return .invalid(errors: validatorResultErrors + validatorErrors)
}
}
}
}
}
We’re going to use reduce
to help with this. It will iterate through all of the validators and call validate
on them. If it is valid, it will just return whatever the previous result was. If it’s invalid, it will return a new error, concatenating the new found errors with any previous ones.
Example of it used
1
2
3
4
5
6
7
let validatorConfigurator = ValidatorConfigurator.sharedInstance
let passwordValidator = validatorConfigurator.passwordValidator()
print(passwordValidator.validate("paSs"))
print(passwordValidator.validate("password"))
print(passwordValidator.validate("passw0rd"))
print(passwordValidator.validate("paSSw0rd"))
This will print the output:
1
2
3
4
invalid([PasswordValidatorError.tooShort, PasswordValidatorError.noUppercaseLetter, PasswordValidatorError.noNumber])
invalid([PasswordValidatorError.noUppercaseLetter, PasswordValidatorError.noNumber])
invalid([PasswordValidatorError.noUppercaseLetter])
valid
Conclusion
There are many ways to take this pattern of using composite validators and modify it to fit the different needs an application might have. Being able to return multiple errors can be essential in order to provide useful feedback to users.
Hit me up on twitter or my email linked below with any feedback!
Take a look at Part 2 of extending composite validators to take advantage of OrCompositeValidators