Last active
July 7, 2022 09:34
-
-
Save ts95/5f8593492a6a162871aaff38311c3667 to your computer and use it in GitHub Desktop.
A Swift protocol for making a model validatable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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