I've found a workaround for the absence of variadic generics key paths using dynamicMemberLookup that I thought it would be interesting to share
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