Validate Temporary Form Models

null

Ian Keen posted an article about type-safe temporary models. You would use them like scratch pad contexts in Core Data: in forms, you collect information into these temporary models and then generate your real objects from them.

Update 2018-06-16: I wrote a post about a nicer validation sub-type.

What's that good for? Imagine a form to edit a user's name. If you pass in the real entity, which might be a reference type aka class, then the form would perform changes on the real object immediately. But what if you want to cancel? How do you get the original values back? That's why you don't use the real entity but a placeholder instead. That's why I proposed a separate read model and a write model a couple of years ago.

Usually, I write these temporary models myself for ever form. Thanks to Swift, this is super easy to do, and you can tie the temporary models closely to the forms with primitive String and Int properties, much like good view models, but for output. ("Data Transfer Object" would be another classical term.) But I like how the Partial<T> Ian came up with works. It uses Swift key paths, so unlike a String-based dictionary, this is pretty type-safe already. And it is super flexible: you don't have to add or remove properties in the temporary model when you change the target model.

Here's part of the example from Ian so you get an impression:

struct User {
    let firstName: String
    let lastName: String
}

var partial = Partial<User>()
partial.update(\.firstName, to: "Ian")

Afterwards, the partial contains information on firstName, but not on lastName. You can create a failable initializer like this:

extension User {
    init(from partial: Partial<User>) throws {
        self.firstName = try partial.value(for: \.firstName)
        self.lastName = try partial.value(for: \.lastName)
    }
}

That would throw an error because there's no value for the key path to lastName. Nice!

I figured you'd want to validate the form input before trying to initialize the model object. So I played around creating a validator.

let validator = Partial<User>.Validation(requiredKeyPaths: \User.firstName, \User.lastName)

switch validator.validate(partial)
case .valid: 
    print("Is valid!")
    do {
        let user = try User(from: partial)
    } catch {
        print("Totally unexpected problem")
    }

case .incomplete(let missingKeyPaths):
    if missingKeyPaths.contains(\.firstName) { print("Missing first name") }
    if missingKeyPaths.contains(\.lastName)  { print("Missing last name") }
}

Instead of printing, you would scroll to and highlight the form fields that have to be completed, of course.

The Validation type is a pretty simple extension:

extension Partial {
    struct Validation {
        let requiredKeyPaths: Set<PartialKeyPath<T>>

        init(requiredKeyPaths first: PartialKeyPath<T>, _ rest: PartialKeyPath<T>...) {
            var all = [first]
            all.append(contentsOf: rest)
            requiredKeyPaths = Set(all)
        }

        func validate(_ partial: Partial<T>) -> Result {
            let intersection = requiredKeyPaths.intersection(Set(partial.data.keys))
            if intersection == requiredKeyPaths { return .valid }
            let difference = requiredKeyPaths.subtracting(intersection)
            return .missing(Array(difference))
        }

        enum Result {
            case valid
            case incomplete([PartialKeyPath<T>])
        }
    }
}

And for posterity, here's Ian's Partial<T> code itself so you have the complete picture:

struct Partial<T> {
    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
    }
}

Browse the blog archive