|
import Foundation |
|
|
|
enum Trait { |
|
case search |
|
case name |
|
case number |
|
case email |
|
case phone |
|
case password |
|
case sensitive |
|
case minLength(Int) |
|
case maxLength(Int) |
|
case range(ClosedRange<Int>) |
|
case validated(Validator) |
|
} |
|
|
|
extension Trait: CustomStringConvertible { |
|
var description: String { |
|
let result: String |
|
switch self { |
|
case .search: |
|
result = "search" |
|
case .name: |
|
result = "name" |
|
case .number: |
|
result = "number" |
|
case .email: |
|
result = "email" |
|
case .phone: |
|
result = "phone" |
|
case .password: |
|
result = "password" |
|
case .sensitive: |
|
result = "sensitive" |
|
case .minLength(let length): |
|
result = "Min Length: \(length)" |
|
case .maxLength(let length): |
|
result = "Max Length: \(length)" |
|
case .range(let range): |
|
result = "Range: \(range.lowerBound) to \(range.upperBound)" |
|
case .validated(let validator): |
|
result = "validated (\(validator.label))" |
|
} |
|
return result |
|
} |
|
} |
|
|
|
struct ValidatorResult { |
|
var isValid: Bool |
|
var reason: String? |
|
} |
|
|
|
protocol Validator { |
|
var label: String { get } |
|
func validate(value: Any) -> ValidatorResult |
|
} |
|
|
|
struct EmailValidator: Validator { |
|
var label: String { "email" } |
|
|
|
func validate(value: Any) -> ValidatorResult { |
|
guard let string = value as? String else { |
|
return ValidatorResult(isValid: false, reason: "Invalid type for email") |
|
} |
|
|
|
let isValid = string.contains("@") && string.count >= 5 |
|
if !isValid { |
|
return ValidatorResult(isValid: false, reason: "Invalid email format") |
|
} else { |
|
return ValidatorResult(isValid: true, reason: nil) |
|
} |
|
} |
|
} |
|
|
|
struct PasswordValidator: Validator { |
|
var label: String { "password" } |
|
|
|
func validate(value: Any) -> ValidatorResult { |
|
guard let string = value as? String else { |
|
return ValidatorResult(isValid: false, reason: "Invalid type for password") |
|
} |
|
|
|
let isValidLength = string.count >= 6 || string.count <= 20 |
|
if !isValidLength { |
|
return ValidatorResult(isValid: false, reason: "Password must be between 6 and 20 characters") |
|
} else { |
|
return ValidatorResult(isValid: true, reason: nil) |
|
} |
|
} |
|
} |
|
|
|
protocol TraitPropertiesValidator { |
|
func validate() -> [ValidatorResult] |
|
} |
|
|
|
extension TraitPropertiesValidator { |
|
func validate() -> [ValidatorResult] { |
|
var results: [ValidatorResult] = [] |
|
let mirror = Mirror(reflecting: self) |
|
for child in mirror.children { |
|
if let traitsProperty = child.value as? TraitsProperty { |
|
results.append(contentsOf: traitsProperty.validate()) |
|
} |
|
} |
|
return results |
|
} |
|
} |
|
|
|
extension Array where Element == ValidatorResult { |
|
var isValid: Bool { |
|
let firstInvalid = first(where: { |
|
$0.isValid == false |
|
}) |
|
return firstInvalid == nil |
|
} |
|
|
|
var invalidReasons: [String] { |
|
filter { !$0.isValid } |
|
.map { |
|
"\($0.reason ?? "Unknown")" |
|
} |
|
} |
|
|
|
func printInvalidResults() { |
|
invalidReasons.forEach { |
|
print($0) |
|
} |
|
} |
|
} |
|
|
|
@propertyWrapper |
|
struct Traits<Value> { |
|
let traits: [Trait] |
|
var wrappedValue: Value |
|
|
|
var projectedValue: Traits { self } |
|
|
|
enum CodingKeys: String, CodingKey { |
|
case wrappedValue |
|
} |
|
|
|
init(wrappedValue: Value, _ traits: [Trait]) { |
|
self.wrappedValue = wrappedValue |
|
self.traits = traits |
|
} |
|
|
|
var isValid: Bool { |
|
true |
|
} |
|
} |
|
|
|
protocol TraitsProperty { |
|
var value: Any { get } |
|
var traits: [Trait] { get } |
|
func validate() -> [ValidatorResult] |
|
} |
|
|
|
extension Traits: TraitsProperty { |
|
var value: Any { |
|
wrappedValue |
|
} |
|
|
|
func validate() -> [ValidatorResult] { |
|
var results: [ValidatorResult] = [] |
|
traits.forEach { trait in |
|
switch trait { |
|
case .validated(let validator): |
|
results.append(validator.validate(value: value)) |
|
default: |
|
break |
|
} |
|
} |
|
return results |
|
} |
|
} |
|
|
|
protocol MirrorPrintable { |
|
func printMirror() |
|
} |
|
|
|
extension MirrorPrintable { |
|
func printMirror() { |
|
let mirror = Mirror(reflecting: self) |
|
for child in mirror.children { |
|
print("Property name:", child.label ?? "") |
|
if let traitsProperty = child.value as? TraitsProperty { |
|
print("Property Value: ", traitsProperty.value) |
|
print("Type:", type(of: traitsProperty.value)) |
|
print("Traits: ", traitsProperty.traits) |
|
} else { |
|
print("Property Value:", child.value) |
|
print("Type:", type(of: child.value)) |
|
} |
|
print("") |
|
} |
|
} |
|
} |
|
|
|
public struct Person { |
|
@Traits([.name]) |
|
var name: String = "" |
|
|
|
@Traits([.email, .sensitive, .validated(EmailValidator())]) |
|
var email: String = "" |
|
|
|
@Traits([.password, .sensitive, .validated(PasswordValidator())]) |
|
var password: String = "" |
|
|
|
var color: String = "" |
|
|
|
var count: Int = 0 |
|
} |
|
|
|
extension Person: MirrorPrintable {} |
|
|
|
extension Person: TraitPropertiesValidator {} |
|
|
|
let person = Person(name: "Jeff", email: "", password: "", color: "Yellow", count: 7) |
|
person.printMirror() |
|
let results = person.validate() |
|
if !results.isValid { |
|
results.printInvalidResults() |
|
} |
|
|
|
// add projectedValue to use $ syntax |
|
//print(person.$name.traits) |