Skip to content

Instantly share code, notes, and snippets.

@DivineDominion
Last active July 5, 2021 10:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DivineDominion/c47706a71cd1046e9ee9e91d4ae1ab22 to your computer and use it in GitHub Desktop.
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
// - 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
}
}
@niorko
Copy link

niorko commented Jul 5, 2021

public struct Partial<T> where T: PartialInitializable {
    
    // MARK: - Errors
    
    public enum Error: Swift.Error {
        case valueNotFound(PartialKeyPath<T>)
    }
    
    // MARK: - Private properties
    
    private var valueData: [PartialKeyPath<T>: Any] = [:]
    
    // MARK: - Init
    
    public init() { }
    
    // MARK: - Interface
    
    public mutating func update<U>(_ keyPath: KeyPath<T, U>, to newValue: U?) {
        valueData[keyPath] = newValue
    }
    
    public func value<U>(for keyPath: KeyPath<T, U>) throws -> U {
        guard let value = valueData[keyPath] as? U else {
            throw Error.valueNotFound(keyPath)
        }
        return value
    }
    
    public func value<U>(for keyPath: KeyPath<T, U?>) -> U? {
        valueData[keyPath] as? U
    }
}

// MARK: - Validation

extension Partial {
    
    public struct Validation {
        
        public 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 :)
            public 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.
            public 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.valueData[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]

        // MARK: - Init Validation
        
        public init(_ first: Strategy, _ rest: Strategy...) {
            var all = [first]
            all.append(contentsOf: rest)
            validations = all
        }

        // MARK: - Interface
        
        public func validate(_ partial: Partial<T>) -> Result {
            var failureReasons: [Result.Reason] = []
            
            for validation in validations {
                switch validation {
                case .required(let keyPath):
                    if !partial.valueData.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)
            }

            do {
                let result = try T.init(from: partial)
                return .valid(result)
            } catch Partial.Error.valueNotFound(let keyPath) {
                return .invalid([.unableToInit(keyPath)])
            } catch {
                return .invalid([.unableToInit(nil)])
            }
        }

        // MARK: - Validation Result type
        
        public enum Result {
            case valid(T)
            case invalid([Reason])
            
            public enum Reason {
                case missing(PartialKeyPath<T>)
                case invalidValue(PartialKeyPath<T>)
                case unableToInit(PartialKeyPath<T>?)
            }
        }
    }
}

@DivineDominion

I added a new Validation.Result case for invalid init, to get rid of return .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 my unableToInit result case implementation. When valueNotFound is thrown with particular PartialKeyPath. 🤷‍♂️

Edit: I am thinking about removing PartialInitializable requirement from Partial. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment