Instantly share code, notes, and snippets.

Embed
What would you like to do?
Extension to @IanKeen's Partial<T> for more sexy and very verbose validations
// - MARK: Example Code
/// A model
struct User {
let firstName: String
let lastName: String
let age: Int?
}
/// Initializer using a Partial<T> -- You could generate this with Sourcery
extension User: PartialInitializable {
init(from partial: Partial<User>) throws {
self.firstName = try partial.value(for: \.firstName)
self.lastName = try partial.value(for: \.lastName)
self.age = partial.value(for: \.age)
}
}
// MARK: Try to create an object from a Partial
var partial = Partial<User>()
partial.update(\.firstName, to: "foo")
let validation = Partial<User>.Validation(
.required(\User.firstName),
.required(\User.lastName),
.valueValidation(keyPath: \User.firstName, { $0.count > 5 }))
// Uncomment to make it pass:
// partial.update(\.lastName, to: "bar")
switch validation.validate(partial) {
case .valid(let user):
print("Is valid: \(user)")
case .invalid(let reasons):
for reason in reasons {
switch reason {
case .missing(let keyPath):
if keyPath == \User.firstName { print("Missing first name") }
if keyPath == \User.lastName { print("Missing last name") }
case .invalidValue(let keyPath):
if keyPath == \User.firstName { print("Invalid first name value") }
if keyPath == \User.lastName { print("Invalid last name value") }
}
}
}
// ⚠️ Won't work in a Playground
// - MARK: The Extension
extension Partial {
struct Validation {
enum Strategy {
case required(PartialKeyPath<T>)
case value(AnyValueValidation)
/// Because constructing AnyValueValidation with the generic ValueValidation<V> inside
/// will get on your nerves, this is a much quicker way to create the objects :)
static func valueValidation<V>(keyPath: KeyPath<T, V>, _ block: @escaping (V) -> Bool) -> Strategy {
let validation = ValueValidation(keyPath: keyPath, block)
return .value(AnyValueValidation(validation))
}
/// Type erasing box (is that the term?) so that the associated value of
/// Strategy.value is itself a simple, non-generic type.
struct AnyValueValidation {
let keyPath: PartialKeyPath<T>
private let _isValid: (Any) -> Bool
init<V>(_ base: ValueValidation<V>) {
keyPath = base.keyPath
_isValid = {
guard let value = $0 as? V else { return false }
return base.isValid(value)
}
}
func isValid(partial: Partial<T>) -> Bool {
guard let value = partial.data[keyPath] else { return false }
return _isValid(value)
}
}
struct ValueValidation<V> {
let keyPath: KeyPath<T, V>
let isValid: (V) -> Bool
init(keyPath: KeyPath<T, V>, _ isValid: @escaping (V) -> Bool) {
self.keyPath = keyPath
self.isValid = isValid
}
}
}
let validations: [Strategy]
init(_ first: Strategy, _ rest: Strategy...) {
var all = [first]
all.append(contentsOf: rest)
validations = all
}
func validate(_ partial: Partial<T>) -> Result {
var failureReasons: [Result.Reason] = []
for validation in validations {
switch validation {
case .required(let keyPath):
if !partial.data.keys.contains(keyPath) {
failureReasons.append(.missing(keyPath))
}
case .value(let valueValidation):
if !valueValidation.isValid(partial: partial) {
failureReasons.append(.invalidValue(valueValidation.keyPath))
}
}
}
guard failureReasons.isEmpty else { return .invalid(failureReasons) }
return .valid(try! T.init(from: partial))
}
enum Result {
case valid
case invalid([Reason])
enum Reason {
case missing(PartialKeyPath<T>)
case invalidValue(PartialKeyPath<T>)
}
}
}
}
// Original code from <http://iankeen.tech/2018/06/05/type-safe-temporary-models/>
protocol PartialInitializable {
init(from partial: Partial<Self>) throws
}
struct Partial<T> where T: PartialInitializable {
enum Error: Swift.Error {
case valueNotFound
}
private var data: [PartialKeyPath<T>: Any] = [:]
mutating func update<U>(_ keyPath: KeyPath<T, U>, to newValue: U?) {
data[keyPath] = newValue
}
func value<U>(for keyPath: KeyPath<T, U>) throws -> U {
guard let value = data[keyPath] as? U else { throw Error.valueNotFound }
return value
}
func value<U>(for keyPath: KeyPath<T, U?>) -> U? {
return data[keyPath] as? U
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment