Property wrappers with generics vs mirroring

Hello, this is my first post here, so please don't be harsh if I did it wrong :blush:

So, I have a property wrapper that is used to define the key in a key-value pair.

@propertyWrapper
final public class KeyValuePair<T> {
    public var wrappedValue: T?
    private(set) var key: String

    public init(key: String) {
        self.key = key
    }
}

And using the following protocol:

protocol Parametrized {
    var toParameters: [String: Any] { get }
}

the property wrapper is used in the following fashion:

struct Person: Parametrized {
    @KeyValuePair(key: "first_name") var firstName: String?
    @KeyValuePair(key: "last_name") var lasteName: String?
    @KeyValuePair(key: "age") var age: Int?
    @KeyValuePair(key: "isSingle") var isSingle: Bool?
    @KeyValuePair(key: "isOpenForDating") var isOpenForDating: Bool?
}

var missUniverse2022 = Person()
missUniverse2022.firstName = "Miss"
missUniverse2022.lastName = "Universe"
missUniverse2022.age = 23
missUniverse2022.isSingle = true // Rad <3
missUniverse2022.isOpenForDating = false // Sad :D

So, the whole idea is to generate a dictionary of type [String: Any] that holds the key-value pairs from that struct using Mirror.
The problem is that the following code is not working (I know why, but I can't figure out the workaround):

extension Parametrized {
    var toParameters: [String: Any] {
        var parameters = [String: Any]()
        
        for child in Mirror(reflecting: self).children {
            
/***********************************************
Generic parameter 'T' could not be inferred in cast to 'KeyValuePair'
          
Explicitly specify the generic arguments to fix this issue
***********************************************/
            guard let kvPair = child.value as? KeyValuePair else { continue }
            
            parameters[kvPair.key] = kvPair.wrappedValue
        }
        
        return parameters
    }
}

So, I turn to the experts here: how to use the mirror logic to get the k-v pairs and put them in a collection?

Thanks in advance!

1 Like

I would type-erase KeyValuePair<T>.

public class AnyKeyValuePair {
    var value: Any? { fatalError() }
    final var key: String
    
    public init(key: String) { self.key = key }
}

final public class KeyValuePair<T>: AnyKeyValuePair {
  public override var value: Any? { wrappedValue }

  ..
}

..

guard let kvPair = child.value as? AnyKeyValuePair else { continue }

You might want to be careful around Any?, though.

PS

You could use another protocol, but since its base class is unused, might as well.

2 Likes

The main issue is that you need to specify the generic type T of KeyValuePair to be able to cast it. In your case, you are not interested in the generic type. However, Swift currently doesn't allow you to omit the generic parameter. To workaround this limitation you need to cast to a type which doesn't have the generic.

You can do it as @Lantua said or use a protocol and make KeyValuePair a struct, which I would highly recommend you to persevere value semantics.
The protocol would look like this:

protocol AnyKeyValuePair {
    var key: String { get }
    var valueAsAny: Any { get }
}

extension KeyValuePair: AnyKeyValuePair {
    var valueAsAny: Any { wrappedValue as Any }
}

and your implementation of toParameters can then just cast child.value as? AnyKeyValuePair

extension Parametrized {
    var toParameters: [String: Any] {
        var parameters = [String: Any]()
        
        for child in Mirror(reflecting: self).children {
            guard let kvPair = child.value as? AnyKeyValuePair else { continue }
            
            parameters[kvPair.key] = kvPair.valueAsAny
        }
        
        return parameters
    }
}
Full Solution
@propertyWrapper
struct KeyValuePair<T> {
    public var wrappedValue: T?
    private(set) var key: String

    public init(key: String) {
        self.key = key
    }
}

protocol AnyKeyValuePair {
    var key: String { get }
    var valueAsAny: Any { get }
}

extension KeyValuePair: AnyKeyValuePair {
    var valueAsAny: Any { wrappedValue as Any }
}

protocol Parametrized {
    var toParameters: [String: Any] { get }
}

struct Person: Parametrized {
    @KeyValuePair(key: "first_name") var firstName: String?
    @KeyValuePair(key: "last_name") var lastName: String?
    @KeyValuePair(key: "age") var age: Int?
    @KeyValuePair(key: "isSingle") var isSingle: Bool?
    @KeyValuePair(key: "isOpenForDating") var isOpenForDating: Bool?
}

var missUniverse2022 = Person()
missUniverse2022.firstName = "Miss"
missUniverse2022.lastName = "Universe"
missUniverse2022.age = 23
missUniverse2022.isSingle = true // Rad <3
missUniverse2022.isOpenForDating = false // Sad :D

extension Parametrized {
    var toParameters: [String: Any] {
        var parameters = [String: Any]()
        
        for child in Mirror(reflecting: self).children {
            guard let kvPair = child.value as? AnyKeyValuePair else { continue }
            
            parameters[kvPair.key] = kvPair.valueAsAny
        }
        
        return parameters
    }
}

print(missUniverse2022.toParameters)
// ["isOpenForDating": Optional(false), "first_name": Optional("Miss"), "last_name": Optional("Universe"), "isSingle": Optional(true), "age": Optional(23)]

Thanks. It works. That protocol was the missing link. Thanks again!

1 Like