Skip to content

Instantly share code, notes, and snippets.

@ts95
Last active July 7, 2022 09:34
Show Gist options
  • Save ts95/5f8593492a6a162871aaff38311c3667 to your computer and use it in GitHub Desktop.
Save ts95/5f8593492a6a162871aaff38311c3667 to your computer and use it in GitHub Desktop.
A Swift protocol for making a model validatable
import Foundation
protocol Rule {
associatedtype Option
var errorMessage: String { get }
init(_ option: Option, error: String)
func validate(for value: ValidatableType) -> Bool
}
struct StringRule: Rule {
let option: Option
let errorMessage: String
init(_ option: Option, error: String) {
self.option = option
self.errorMessage = error
}
func validate(for value: ValidatableType) -> Bool {
guard let value = value as? String else { return false }
switch option {
case .notEmpty:
return !value.isEmpty
case .max(let maxValue):
return value.count <= maxValue
case .min(let minValue):
return value.count >= minValue
case .regex(let regex):
if value.isEmpty { return true }
return value.range(of: regex, options: .regularExpression) != nil
}
}
enum Option {
case notEmpty
case max(Int)
case min(Int)
case regex(String)
}
}
struct IntRule: Rule {
let option: Option
let errorMessage: String
init(_ option: Option, error: String) {
self.option = option
self.errorMessage = error
}
func validate(for value: ValidatableType) -> Bool {
guard let value = value as? Int else { return false }
switch option {
case .notZero:
return value != 0
case .max(let maxValue):
return value <= maxValue
case .min(let minValue):
return value >= minValue
}
}
enum Option {
case notZero
case max(Int)
case min(Int)
}
}
protocol ValidatableType { }
extension String: ValidatableType { }
extension Int: ValidatableType { }
class Validator {
let label: String
private let _validate: () -> [String]
var errorMessages: [String] {
return _validate()
}
init<V: ValidatableType, R: Rule>(_ label: String, _ value: V?, rules: [R]) {
self.label = label
self._validate = {
return rules
.filter { value == nil ? false : !$0.validate(for: value!) }
.map { $0.errorMessage }
}
}
}
protocol Validatable {
var valid: Bool { get }
var invalid: Bool { get }
var errors: [String : [String]] { get }
var validators: [Validator] { get }
}
extension Validatable {
var valid: Bool {
return errors.count == 0
}
var invalid: Bool {
return !valid
}
var errors: [String : [String]] {
var dict = [String : [String]]()
for validator in validators {
let errorMessages = validator.errorMessages
if !errorMessages.isEmpty {
dict[validator.label] = errorMessages
}
}
return dict
}
}
////////////////////////////////
// TESTING
////////////////////////////////
struct Person {
var name: String
var age: Int?
}
extension Person: Validatable {
var validators: [Validator] {
return [
Validator("name", self.name, rules: [
StringRule(.notEmpty, error: "Name can't be empty."),
StringRule(.max(20), error: "Name can't be longer than 20 characters."),
StringRule(.regex("^\\b[a-zA-Z-]+\\b$"), error: "Name contains invalid characters."),
]),
Validator("age", self.age, rules: [
IntRule(.notZero, error: "Age can't be zero."),
IntRule(.max(150), error: "Age can't be greater than 150."),
]),
]
}
}
let person = Person(name: "", age: 0)
if person.invalid {
print(person.errors)
// -> ["name": ["Name can't be empty."], "age": ["Age can't be zero."]]
}
let person2 = Person(name: "A person with a name that is waaaay too long +&%$", age: nil)
if person2.invalid {
print(person2.errors)
// -> ["name": ["Name can't be longer than 20 characters.", "Name contains invalid characters."]]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment