scottrobbins.dev

Get In Touch

Prefer using email? Say hi at hello@jekyllthemes.io

Extending your Composite Validators - Part 2

Separating AndCompositeValidator and OrCompositeValidator

 

If you haven’t read my post on Composite Validators and my post on returning multiple errors from them, take a look at those first, as this is going to build off of what was created in those posts.


What’s the problem?

All of the user input we have been validating thus far had rules that were all required to pass in order for the input to be valid. A password had to have a lowercase letter and an uppercase letter and a number.

There are many scenarios in which you may only need some combination of the validators to pass in order for the input to be considered valid. For example: perhaps you have a field for a phone number that is optional. If they enter a phone number, you want to make sure it’s the correct number of digits, correct characters, etc. However, if they don’t enter anything at all, that is also valid.

You want the phone number to be valid or an empty phone number.

Our Example

The example I’m going to show will expand upon the password validator, since by now you are familiar with that and more importantly I am lazy.

As a refresh on the requirements for passwords, they:

  • Must not be empty
  • Must be at least 8 characters long
  • Must have one uppercase letter, lowercase letter and a number

I’m going to expand that last rule to be:

  • Must have one uppercase letter
  • Must have one lowercase letter
  • Must have either a number or a special character

And Composite Validator

What we were previously calling CompositeValidator I am just going to rename to AndCompositeValidator.

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 AndCompositeValidator: 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)
                }
            }
        }
    }
}

Or Composite Validator

For this one, if any of the validators passed in return a valid result, than the OrCompositeValidator will return that the input is valid. Otherwise, every error will be returned in the array with the invalid response.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct OrCompositeValidator: Validator {

    private let validators: [Validator]

    init(validators: Validator...) {
        self.validators = validators
    }

    func validate(_ value: String) -> ValidatorResult {
        return validators.reduce(.invalid(errors: [])) { validatorResult, validator in
            guard case .invalid(let validatorResultErrors) = validatorResult else {
                return .valid
            }

            switch validator.validate(value) {
            case .valid:
                return .valid
            case .invalid(let validatorErrors):
                return .invalid(errors: validatorResultErrors + validatorErrors)
            }
        }
    }
}

Validator Configurator

This is just a class I use to instantiate validators.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct ValidatorConfigurator {

    // Interface

    static let sharedInstance = ValidatorConfigurator()

    func passwordValidator() -> Validator {
        return AndCompositeValidator(validators: emptyPasswordStringValidator(),
                                  passwordStrengthValidator())
    }

    // Helper methods

    private func emptyPasswordStringValidator() -> Validator {
        return EmptyStringValidator(invalidError: PasswordValidatorError.empty)
    }

    private func passwordStrengthValidator() -> Validator {
        return AndCompositeValidator(validators: PasswordLengthValidator(),
                                  UppercaseLetterValidator(),
                                  LowercaseLetterValidator(),
                                  numberOrSpecialCharacterValidator())
    }

    private func numberOrSpecialCharacterValidator() -> Validator {
        return OrCompositeValidator(validators: ContainsNumberValidator(),
                                    ContainsSpecialCharacterValidator())
    }
}

I didn’t bother showing the special character validator. It’s very similar to the others, just different regex.

Example of it used

1
2
3
4
5
6
7
let validatorConfigurator = ValidatorConfigurator.sharedInstance
let passwordValidator = validatorConfigurator.passwordValidator()

print(passwordValidator.validate("Password"))
print(passwordValidator.validate("Password1"))
print(passwordValidator.validate("Password$"))
print(passwordValidator.validate("Password1$"))

This will print the output:

1
2
3
4
invalid([PasswordValidatorError.noNumber, PasswordValidatorError.noSpecialCharacter])
valid
valid
valid

Conclusion

There are many modifications and additions that can be added to this pattern to make it more powerful and fit the needs of an application. To get more ideas, you can look at Microsoft’s specification pattern, which is what a lot of this is based on.

This is going to end my series on composite validators. Hope you found it interesting. Tag me on twitter or email me with any feedback!