Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do You Convert an AnyKeyPath to a WritableKeyPath?

I have an array of enum cases, where each case has a keyPath property, which returns an AnyKeyPath matching the classes property with the same name as the enum case:

protocol PathAccessor: CodingKey {
    var keyPath: AnyKeyPath { get }
    static var allCases: [Self] { get }

    init?(rawValue: Int)
}

extension PathAccessor {
    static var allCases: [Self] {
        var cases: [Self] = []
        var index: Int = 0
        while let element = Self.init(rawValue: index) {
            cases.append(element)
            index += 1
        }

        return cases
    }
}

class Robot {

    let name: String
    var age: Int
    var powered: Bool
    var hasItch: Bool?

    enum CodingKeys: Int, PathAccessor {
        case name
        case age
        case powered
        case hasItch

        var keyPath: AnyKeyPath {
            switch self {
            case .name: return \Robot.name
            case .age: return \Robot.age
            case .powered: return \Robot.powered
            case .hasItch: return \Robot.hasItch
            }
        }
    }

    init(name: String, age: Int, powered: Bool) {
        self.name = name
        self.age = age
        self.powered = powered
    }
}

for element in Robot.CodingKeys.allCases {
    // Trying to implement
}

In the loop above, I want to check the keyPath property of the case to see if it is a WritableKeyPath, and if it is, create a closure that will modify the property that the key path accesses.

The problem with this is that a WritableKeyPath is a generic type. I know the Root type, but the Value type could be almost any type in existence. I could create a bunch of cases for each of most likely types:

if let path = element.keyPath as? WritableKeyPath<Robot, Int> {

} else if let path = element.keyPath as? WritableKeyPath<Robot, String> {

} // So on and so forth

But that is time consuming, ugly, and hard to maintain.

I did try to cast to a dynamic type, but that gives a compiler error (Use of undeclared type 'valueType'):

let valueType = type(of: element.keyPath).valueType
guard let path = element.keyPath as? WritableKeyPath<Self, valueType> else {
    continue
}

I could use a protocol that the types already conform to, but for some reason, that is also failing:

guard let path = element.keyPath as? WritableKeyPath<Robot, NodeInitializable> else {
    print("bad")
    continue
}
print("good")

// Output:
// bad
// bad
// bad
// bad

So, is it even possible to convert an AnyKeyPath to a WritableKeyPath without a huge string of unwrapping statements or weird hacks that shouldn't be used in production?

like image 644
Caleb Kleveter Avatar asked Nov 17 '22 22:11

Caleb Kleveter


1 Answers

After a few hours playing around with code, the best I got was this:

struct Person {
var name: String
var collection: [String]
var optionalCollection: [String]?
let birthdate: Date

fileprivate func keyPath<V>(from label: String, type: V.Type) -> WritableKeyPath<Person, V> {
    print("type: \(type)")
    let valuePair: [String: PartialKeyPath<Person>] = ["name": \Person.name,
                                                       "birthdate": \Person.birthdate,
                                                       "collection": \Person.collection,
                                                       "optionalCollection": \Person.optionalCollection]
    return valuePair[label] as! WritableKeyPath<Person, V>
}


}



var person = Person(name: "john",
                    collection: [],
                    optionalCollection: ["gotcha"],
                    birthdate: Date(timeIntervalSince1970: 0))


let name = person[keyPath: person.keyPath(from: "name", type: String.self)] // john
let birthdate = person[keyPath: person.keyPath(from: "birthdate", type: Date.self)] // Jan 1, 1970
let collection = person[keyPath: person.keyPath(from: "collection", type: Array<String>.self)] // []
let optionalCollection = person[keyPath: person.keyPath(from: "optionalCollection", type: Optional<Array<String>>.self)] // ["gotcha"]

However you must always pass the type as parameter. If only the Mirror class allowed us to get the actual type from each property.

like image 132
RicardoDuarte Avatar answered Jan 17 '23 12:01

RicardoDuarte