Skip to content

Instantly share code, notes, and snippets.

@jon-cotton
Last active May 31, 2016 09:30
Show Gist options
  • Save jon-cotton/ae57aeac4c9a9d9e59b1025d05013dc5 to your computer and use it in GitHub Desktop.
Save jon-cotton/ae57aeac4c9a9d9e59b1025d05013dc5 to your computer and use it in GitHub Desktop.
Basic validation 'engine' for Swift, allows you to quickly test a group of UITextFields against either basic built in validation rules or custom regex patterns and get back a group of specfic errors as to what didn't match/validate. Copy and paste the raw contents into a Swift playground to try it out for yourself.
//: Playground - noun: a place where people can play
import UIKit
protocol Validator {
associatedtype T
func isValid(value: T) throws -> Bool
}
protocol Validateable {
associatedtype ValidatorType: Validator
func validValue(validators: ValidatorType...) throws -> ValidatorType.T
}
extension Validateable where ValidatorType.T == Self {
func validValue(validators: [ValidatorType]) throws -> Self {
var errors = AggregateError()
for validator in validators {
do {
try validator.isValid(self)
} catch {
errors.addError(error)
}
}
guard errors.isEmpty else {
throw errors
}
return self
}
func validValue(validators: ValidatorType...) throws -> Self {
return try validValue(validators)
}
}
extension Optional where Wrapped: Validateable, Wrapped.ValidatorType.T == Wrapped {
func validValue(validators: [Wrapped.ValidatorType]) throws -> Wrapped {
switch self {
case .None:
throw ValidationError.valueIsNil
case .Some(let value):
return try value.validValue(validators)
}
}
func validValue(validators: Wrapped.ValidatorType...) throws -> Wrapped {
return try validValue(validators)
}
}
// MARK:- Validation Errors
struct AggregateError: ErrorType {
private(set) var errors: [ErrorType] = []
mutating func addError(error: ErrorType) {
errors.append(error)
}
var isEmpty: Bool {
return errors.isEmpty
}
}
extension AggregateError: CollectionType {
typealias Index = Int
var startIndex: Int {
return 0
}
var endIndex: Int {
return errors.count
}
subscript(i: Int) -> ErrorType {
return errors[i]
}
}
enum RegexError: ErrorType {
case stringDoesNotMatchRegexPattern
}
enum ValidationError: ErrorType {
case valueIsNil
}
enum StringValidationError: ErrorType {
case stringIsEmpty
case stringContainsNonAlphaCharacters
case stringContainsNonNumericCharacters
case stringContainsNonAlphaNumericCharacters
case stringIsNotAValidEmailAddress
case stringsDoNotMatch
}
enum ComparableValidationError<T>: ErrorType {
case valueIsBelowMinimumBounds(T)
case valueIsAboveMaximumBounds(T)
case lowerBoundsMustBeLessThanUpperBounds
}
// MARK:- Regex Things
protocol RegexPattern {
var pattern: String {get}
var errorToThrow: ErrorType {get}
func match(string: String) throws -> Bool
}
extension RegexPattern {
func match(string: String) throws -> Bool {
guard string =~ pattern else {
throw errorToThrow
}
return true
}
}
extension String: RegexPattern {
var pattern: String {
return self
}
var errorToThrow: ErrorType {
return RegexError.stringDoesNotMatchRegexPattern
}
}
extension RegexPattern where Self: RawRepresentable, Self.RawValue == String {
var pattern: String {
return rawValue
}
}
struct Regex {
let expression: NSRegularExpression
init(_ pattern: RegexPattern) throws {
expression = try NSRegularExpression(pattern: pattern.pattern, options: .DotMatchesLineSeparators)
}
func test(string: String) -> Bool {
let matches = expression.matchesInString(string, options: .ReportCompletion, range: NSMakeRange(0, string.characters.count))
return matches.count > 0
}
}
infix operator =~ {}
func =~ (input: String, pattern: RegexPattern) -> Bool {
do {
return try Regex(pattern).test(input)
} catch {
return false
}
}
// MARK:- Validators
enum StringValidationPattern: String, RegexPattern {
case alphaOnly = "^[a-zA-Z]*$"
case numericOnly = "^[0-9]*$"
case alphaNumericOnly = "^[a-zA-Z0-9]*$"
case email = "^[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])?)*$`"
var errorToThrow: ErrorType {
switch self {
case .alphaOnly: return StringValidationError.stringContainsNonAlphaCharacters
case .numericOnly: return StringValidationError.stringContainsNonNumericCharacters
case .alphaNumericOnly: return StringValidationError.stringContainsNonAlphaNumericCharacters
case .email: return StringValidationError.stringIsNotAValidEmailAddress
}
}
}
enum StringValidator: Validator {
case nonEmpty
case regex(RegexPattern)
case match(String)
func isValid(value: String) throws -> Bool {
let string = value
switch self {
case .nonEmpty:
guard string != "" else {
throw StringValidationError.stringIsEmpty
}
case .regex(let pattern):
try pattern.match(string)
case .match(let toMatch):
guard string == toMatch else {
throw StringValidationError.stringsDoNotMatch
}
}
return true
}
}
enum ComparableValidator<T: Comparable>: Validator {
case minimumValue(T)
case maximumValue(T)
case range(T, T)
func isValid(value: T) throws -> Bool {
var min: T?
var max: T?
switch self {
case .minimumValue(let inMin):
min = inMin
case .maximumValue(let inMax):
max = inMax
case .range(let inMin, let inMax):
min = inMin
max = inMax
guard min < max else {
throw ComparableValidationError<T>.lowerBoundsMustBeLessThanUpperBounds
}
}
if let min = min {
guard value >= min else {
throw ComparableValidationError.valueIsBelowMinimumBounds(min)
}
}
if let max = max {
guard value <= max else {
throw ComparableValidationError.valueIsAboveMaximumBounds(max)
}
}
return true
}
}
// MARK:- String Validation
extension String: Validateable {
typealias ValidatorType = StringValidator
}
// Pass
do {
try "abc".validValue(.regex(StringValidationPattern.alphaOnly), .match("abc"))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// Fail
do {
try "123".validValue(.regex(StringValidationPattern.alphaOnly))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// Pass
do {
try "123".validValue(.regex(StringValidationPattern.numericOnly))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// Pass
do {
try "abc123".validValue(.regex(StringValidationPattern.alphaNumericOnly))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// Pass
do {
try "jon.cotton@test.co.uk".validValue(.regex(StringValidationPattern.email))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// MARK:- Numeric Validation
// This is probably a little OTT as it would be more succint/readable to just do the comparison inline, but is here to demonstrate how generic the pattern is
extension Int: Validateable {
typealias ValidatorType = ComparableValidator<Int>
}
extension Double: Validateable {
typealias ValidatorType = ComparableValidator<Double>
}
extension Float: Validateable {
typealias ValidatorType = ComparableValidator<Float>
}
// Pass
do {
try 100.validValue(.minimumValue(50))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// Fail
do {
try 100.validValue(.minimumValue(400))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// Fail
do {
try 100.0.validValue(.range(0.1, 99.9))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// MARK:- TextField Validation
extension UITextField: Validateable {
typealias ValidatorType = StringValidator
func validValue(validators: StringValidator...) throws -> String {
return try text.validValue(validators)
}
}
let textField = UITextField()
// Fail
textField.text = ""
do {
let value = try textField.validValue(.nonEmpty, .regex(StringValidationPattern.alphaOnly))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// Pass
textField.text = "abc"
do {
let value = try textField.validValue(.nonEmpty, .regex(StringValidationPattern.alphaOnly))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// Fail
textField.text = "abc"
do {
let value = try textField.validValue(.regex(StringValidationPattern.numericOnly), .regex(StringValidationPattern.email))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// Pass
textField.text = "jon.cotton@test.uk"
do {
let value = try textField.validValue(.nonEmpty, .regex(StringValidationPattern.email))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// Fail
textField.text = "not_a_valid_@_email_address.com"
do {
let value = try textField.validValue(.nonEmpty, .regex(StringValidationPattern.email))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
// MARK:- Domain Specific Validation
enum UserValidationError: ErrorType {
case passwordIsUnexpectedFormat
}
enum UserValidationPattern: String, RegexPattern {
case password = "^[a-zA-Z0-9*_\\-&%\\$£@]{8,32}$"
var errorToThrow: ErrorType {
switch self {
case .password: return UserValidationError.passwordIsUnexpectedFormat
}
}
}
// Pass
let confirmationTextField = UITextField()
confirmationTextField.text = "somePa$$word"
textField.text = "somePa$$word"
do {
let value = try textField.validValue(.regex(UserValidationPattern.password), .match(confirmationTextField.text!))
} catch {
error
}
// Fail
confirmationTextField.text = "somePa$$word"
textField.text = "somePa))word"
do {
let value = try textField.validValue(.regex(UserValidationPattern.password), .match(confirmationTextField.text!))
} catch let aggregateError as AggregateError {
aggregateError.errors
} catch {
error
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment