Last active
July 5, 2021 10:22
-
-
Save DivineDominion/c47706a71cd1046e9ee9e91d4ae1ab22 to your computer and use it in GitHub Desktop.
Extension to @IanKeen's Partial<T> for more sexy and very verbose validations
This file contains 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
// - 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") } | |
} | |
} | |
} |
This file contains 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
// ⚠️ 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>) | |
} | |
} | |
} | |
} |
This file contains 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
// 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
@DivineDominion
I added a new
Validation.Result
case for invalid init, to get rid ofreturn .valid(try! T.init(from: partial))
.Another idea that I am playing with is to remove
.required
strategy and deal with this requirement based on myunableToInit
result case implementation. WhenvalueNotFound
is thrown with particularPartialKeyPath
. 🤷♂️Edit: I am thinking about removing
PartialInitializable
requirement fromPartial
. It might help but it's also a double edged sword and it adds additional complexity to the whole Validator and another case to handle.