Composite Validators – Refined

I recently saw a post on how to compose different validators together. I thought it was a good start, but I think it didn’t go quite far enough, especially using the fact that Rob Napier has so kindly reminded us about Swift: types are the unit of composition in Swift.

Here’s the basic setup: there is a need to create some basic validation for email and passwords (the correctness of the validation rules is not important to the discussion).

Scott modeled out what how he conceptually thought of each, which I thought was good. I’ve put those models here, with only some minor name changes:

Email Validation
                ┌────────────────────────┐                
                │    email validation    │                
                └────────────────────────┘                
                             ▲                            
             ┌───────────────┴───────────────┐            
┌────────────────────────┐      ┌────────────────────────┐
│         empty          │      │     invalid format     │
└────────────────────────┘      └────────────────────────┘
Password Validation
                ┌────────────────────────┐          
                │  password validation   │                               
                └────────────────────────┘                               
                             ▲                                           
             ┌───────────────┴───────────────┐                           
┌────────────────────────┐      ┌────────────────────────┐               
│         empty          │      │          weak          │               
└────────────────────────┘      └────────────────────────┘               
                                             ▲                           
                                             │ ┌────────────────────────┐
                                             ├─│         length         │
                                             │ └────────────────────────┘
                                             │ ┌────────────────────────┐
                                             ├─│   missing uppercase    │
                                             │ └────────────────────────┘
                                             │ ┌────────────────────────┐
                                             ├─│   missing lowercase    │
                                             │ └────────────────────────┘
                                             │ ┌────────────────────────┐
                                             └─│     missing number     │
                                               └────────────────────────┘

I think these are conceptually good. However, I wasn’t a fan of how Scott transcribed these models over to their enum representations.

Scott’s Version
enum EmailValidatorError: Error {
    case empty
    case invalidFormat
}

enum PasswordValidatorError: Error {
    case empty
    case tooShort
    case noUppercaseLetter
    case noLowercaseLetter
    case noNumber
}

I think the PasswordValidationError missed the mark as he flattened out the tree.

My Version
enum EmailValidationError: Error {
	case empty
	case invalidFormat
}

enum PasswordValidationError: Error {
	case empty
	case weak(reasoning: [PasswordStrengthValidationError])
}

enum PasswordStrengthValidationError: Error {
	case length
	case missingUppercase
	case missingLowercase
	case missingNumber
}

I break out the weak items into their own PasswordStrengthValidationError error enum to match the conceptual model. This has the benefit of provide a high-level classification of “weak password” while still maintaining the rigor of getting the specific details about why the password is considered weak.

Validation

Next up is the modeling of the validator itself. I like that Scott chose to model this as a protocol. After all, we do know that we are going to need two different types of protocols:

  1. Single validation
  2. Composite validation

However, unlike Scott, I would have modeled both of these with a protocol, like so:

protocol Validator {
	func validate(_ value: String) -> Result
}

protocol CompositeValidator: Validator {
	var validators: [Validator] { get }
	func validate(_ value: String) -> [Result]
}

Side note: I’m also use a more generic Result type vs. Scott’s ValidationResult. It’s simply defined as:

enum Result {
	case ok(ValueType)
	case error(Error)
}

The important thing to note about the CompositeValidator is that it supports returning both a single result and a listing of all of the results. The other nice thing about this is that it’s also possible to provide a default implementation for all implementors of this protocol.

extension CompositeValidator {
	func validate(_ value: String) -> [Result] {
		return validators.map { $0.validate(value) }
	}

	func validate(_ value: String) -> Result {
		let results: [Result] = validate(value)
		let errors = results.filter {
			if case .error(_) = $0 {
				return true
			}
			else {
				return false
			}
		}

		return errors.first ?? .ok(value)
	}
}

In the single result case, we simply return the first error value, if present, or return the ok value. This default implementation will come in really handy when we start implementing things later on.

Types as the Foundation

Remember that types are the foundation? So yeah… let’s start implementing the email validators as they are the most straight-forward of the two.

class EmailEmptyValidator: Validator {
	func validate(_ value: String) -> Result {
		return value.isEmpty ? .error(EmailValidationError.empty) : .ok(value)
	}
}

class EmailFormatValidator: Validator {
	func validate(_ value: String) -> Result {
		let magicEmailRegexStolenFromTheInternet = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"

		let emailTest = NSPredicate(format:"SELF MATCHES %@", magicEmailRegexStolenFromTheInternet)

		return emailTest.evaluate(with: value) ?
			.ok(value) :
			.error(EmailValidationError.invalidFormat)
	}
}

The single value ones are fairly straight forward – you just implement the logic in the validate function based on the value passed in.

Now, to compose the email validations, all we need to do is this:

class EmailValidator: CompositeValidator {
	let validators: [Validator]

	init() {
		self.validators = [
			EmailEmptyValidator(),
			EmailFormatValidator()
		]
	}
}

That’s it! No extra code to worry about as the default CompositeValidator extensions handle this correctly.

The usage code looks like this:

let emailValidator = EmailValidator()
validate(value: "", validator: emailValidator)
validate(value: "invalidEmail@", validator: emailValidator)
validate(value: "validEmail@validDomain.com", validator: emailValidator)

Which will output:

value: "" => error(EmailValidationError.empty)
value: "invalidEmail@" => error(EmailValidationError.invalidFormat)
value: "validEmail@validDomain.com" => ok("validEmail@validDomain.com")

Password Validation

The single value password validators are all straight forward to implement:

class PasswordEmptyValidator: Validator {
	func validate(_ value: String) -> Result {
		return value.isEmpty ? .error(PasswordValidationError.empty) : .ok(value)
	}
}

class PasswordLengthValidator: Validator {
	static let minimumPasswordLength: Int = 8

	func validate(_ value: String) -> Result {
		return value.characters.count >= PasswordLengthValidator.minimumPasswordLength ?
			.ok(value) :
			.error(PasswordStrengthValidationError.length)
	}
}

class PasswordIncludesUppercaseValidator: Validator {
	func validate(_ value: String) -> Result {
		return value.rangeOfCharacter(from: NSCharacterSet.uppercaseLetters) != nil ?
			.ok(value) :
			.error(PasswordStrengthValidationError.missingUppercase)
	}
}

class PasswordIncludesLowercaseValidator: Validator {
	func validate(_ value: String) -> Result {
		return value.rangeOfCharacter(from: NSCharacterSet.lowercaseLetters) != nil ?
			.ok(value) :
			.error(PasswordStrengthValidationError.missingLowercase)
	}
}

class PasswordIncludesNumbersValidator: Validator {
	func validate(_ value: String) -> Result {
		return value.rangeOfCharacter(from: NSCharacterSet.decimalDigits) != nil ?
			.ok(value) :
			.error(PasswordStrengthValidationError.missingNumber)
	}
}

There really is nothing special going on here. However, what does the validation for the weak error type look like? Well, here it is:

class PasswordStrengthValidator: CompositeValidator {
	let validators: [Validator]

	init() {
		self.validators = [
			PasswordLengthValidator(),
			PasswordIncludesUppercaseValidator(),
			PasswordIncludesLowercaseValidator(),
			PasswordIncludesNumbersValidator()
		]
	}

	func validate(_ value: String) -> Result {
		let result = validate(value) as [Result]
		let errors = result.filter { if case .error(_) = $0 { return true }; return false }

		if errors.isEmpty { return .ok(value) }

		let reasons: [PasswordStrengthValidationError] = errors.map {
			if case let .error(reason) = $0 { return reason as! PasswordStrengthValidationError }
			fatalError("This code should never be reached. It is an error if it ever hits.")
		}

		return .error(PasswordValidationError.weak(reasoning: reasons))
	}
}

As you can see, it is necessary to change the default implementation of validate as we would like to do a custom conversion to box the potential list of failures reasons into a single weak value. If we modeled the error type as Scott had them, this step would be unnecessary.

Finally, we look at the all up composite validator for passwords:

class PasswordValidator: CompositeValidator {
	let validators: [Validator]

	init() {
		self.validators = [
			PasswordEmptyValidator(),
			PasswordStrengthValidator()
		]
	}
}

Again, we can simply use the default implementations for our two validate functions as there is no special logic we need here. The usage code is:

let passwordValidator = PasswordValidator()
validate(value: "", validator: passwordValidator)
validate(value: "psS$", validator: passwordValidator)
validate(value: "passw0rd", validator: passwordValidator)
validate(value: "paSSw0rd", validator: passwordValidator)

With the output of:

value: "" => error(PasswordValidationError.empty)
value: "psS$" => error(PasswordValidationError.weak([PasswordStrengthValidationError.length, PasswordStrengthValidationError.missingNumber]))
value: "passw0rd" => error(PasswordValidationError.weak([PasswordStrengthValidationError.missingUppercase]))
value: "paSSw0rd" => ok("paSSw0rd")

In the End

This is really just a refinement of Scott’s approach. I think he started off with the right thought process. The biggest critique I have about his approach is in the end when he needed to use functions to encapsulate the construction the various composite validators. Using functions as constructors can often times mean you simply don’t have the right type abstraction yet. I do believe that is the case here, which is why I introduced the CompositeValidator protocol.

Update: Here’s a link to the full playground source code: https://gist.github.com/owensd/6a99ca908d3c13c8b19c9a42aaf3cd9d.

Composite Validators – Refined

3 thoughts on “Composite Validators – Refined

  1. Great approach, seems useful for a lot of applications. One question – when you write validate(value: "", validator: passwordValidator) as part of your demo code, where is the method validate(value:,validator:) coming from? I didn’t see that defined anywhere, and I was curious why you were wrapping the validator in another method versus just calling, for example, emailValidator.validate("").

    Like

  2. The validate function looks like this:


    func validate(value: String, validator: Validator) {
    print("value: \"(value)\" => (validator.validate(value))")
    }

    I didn’t want to duplicate the value being passed in, and I wanted to be able to show the value in the printed output.

    Like

Comments are closed.