Using dynamicMemberLookup to constraint PartialKeyPath

I've found a workaround for the absence of variadic generics key paths using dynamicMemberLookup that I thought it would be interesting to share :smile:

The Problem

Vapor currently uses the following function to select only a subset of a model's fields in a query.


public final class QueryBuilder<Model> where Model: FluentKit.Model {

    public func field<Field>(_ field: KeyPath<Model, Field>) -> Self where Field: QueryableProperty { ... }

}

Planet.query(on: database).field(\.$id).field(\.$name).all()

To select multiple fields this function has to be called multiple times because we can't have as parameter a variadic number of key paths where the Value type is constrained to a given protocol. Using Variadic Generics it would be something like:


 public func fields<Field...>(_ fields: KeyPath<Model, Field>...) -> Self where Field: QueryableProperty { ... } ❌

The only way to accept a variadic number of key paths of different types is using PartialKeyPath but then we loose the QueryableProperty constraint.


public func fields<Field>(_ field: PartialKeyPath<Model>...) -> { ... } 

The Workaround

It's possible to make a type safe api that accept only key paths constrained by a given protocol using a @dynamicMemberLookup type with type constraint in the subscript(dynamicMember) function.


@dynamicMemberLookup
public struct QueryablePropertiesOf<Model: FluentKit.Model> {

    subscript(dynamicMember keyPath: KeyPath<Model, some QueryableProperty>) -> PartialKeyPath<Model> {
        return keyPath
    }
    
}

public typealias PropertyKeyPath<Model: FluentKit.Model> = PartialKeyPath<QueryablePropertiesOf<Model>>

public func fields(_ fields: PropertyKeyPath<Model>...) -> Self {
    let fields = fields.map({ QueryablePropertiesOf<Model>()[keyPath: $0] as! PartialKeyPath<Model> })
    ...
}

Internally we still have to deal with PartialKeyPath but QueryablePropertiesOf will ensure that the function only accepts KeyPaths of QueryableProperty types.


Planet.query(on: database).fields(\.$id, \.$name) ✅

Planet.query(on: database).fields(\.$id, \.name) ❌ Subscript 'subscript(dynamicMember:)' requires that 'String' conform to 'QueryableProperty'

WORKING CODE EXAMPLE

protocol Model { }

protocol QueryableProperty { }

struct T1: QueryableProperty {}

struct T2: QueryableProperty {}

struct T3 {}

struct M1: Model {
  let a: T1
  let b: T2
  let c: T3
}

@dynamicMemberLookup
struct QueryablePropertiesOf<Root: Model> {

    subscript(dynamicMember keyPath: KeyPath<Root, some QueryableProperty>) -> PartialKeyPath<Root> {
        return keyPath
    }
    
}

typealias PropertyKeyPath<Root: Model> = PartialKeyPath<QueryablePropertiesOf<Root>>

func fields(_ fields: PropertyKeyPath<M1>...) {
  let fields = fields.map({ QueryablePropertiesOf<M1>()[keyPath: $0] as! PartialKeyPath<M1> })
  print(fields)
}

fields(\.a, \.b)

// fields(\.a, \.b, \.c) // ❌ subscript 'subscript(dynamicMember:)' requires that 'T3' conform to 'QueryableProperty'

@codafi I think this is a good motivation for variadic generics pitch
@0xTim Maybe this reading will amuse you

5 Likes