One bane of programming is 'stringly typed' APIs, where strings are used to convey some sort of meaning that would probably be better conveyed with types, but for whatever reason, we can't or don't. A great example of this is code that generates queries against databases.
KeyPaths seem like they should solve this issue, but currently there's no way to examine a KeyPath and figure out what the path is or what it points to - at the moment all you can do with them is use them to access properties.
If KeyPath
had an API to expose the path it expresses, we could do neat stuff...
struct Person {
...
let name: String
let age: Int
...
}
struct Database {
func select<Table, Property, OrderBy>(_ source: KeyPath<Table, Property>, orderBy: KeyPath<Table, OrderBy>) -> [Property] {
let query = "SELECT \(source.pathComponents.first!) FROM \(Table.self) t ORDER BY t.(orderBy.pathComponents.joined(separator: "."))"
return execute(query)
}
}
let database = Database()
let sql = database.select(\Person.name, orderBy: \.age)
That's a pretty convoluted and definitely non-production ready example, but hopefully illustrates the point (and actually compiles if you add stubbed extensions to KeyPath
). I'm not sure what shape the API should take, but it could enable some really interesting and powerful type-safe APIs for query generation, configuration, testing etc.
Expressing database schema details and constraints
As well as generating queries, KeyPaths could also be used to generate or validate schemas or schema-like structures, and allow frameworks to enforce constraints before a request makes it to the database.
Code
struct PersonMappingConfiguration {
func map() {
mapper.hasMany(\.pets)
mapper.makeUnique(\.nickName)
mapper.useColumnName("firstName", forProperty: \.name).makeReadonly()
}
}
Validation
Similarly, KeyPath introspection could be used to create validators. In this case, the validator code becomes stateless.
Code
struct MemberValidation {
let validation = Validator<Member>()
func configure() {
validation.of(\.name).required().maxLength(32)
validation.of(\.age).minValue(1).withErrorMessage(message: { age in "Age \(age) is not valid!"})
validation.of(\.nickName).required().withErrorMessage(message: { member, nickName in "\(member.name)'s nickname '\(nickName)' is too short!" })
}
}
struct Validator<Type> {
func of<Prop>(_ prop: KeyPath<Type, Prop>) -> Validation<Type, Prop> { return Validation<Type, Prop>() }
}
struct Validation<Type, Prop> {
func required() -> Self { ... }
func maxLength(_ maxLength: Int) -> Self { ... }
func minLength(_ maxLength: Int) -> Self { ... }
func minValue(_ minValue: Int) -> Self { ... }
func withErrorMessage(message: (Prop) -> String) -> Self { ... }
func withErrorMessage(message: (Type, Prop) -> String) -> Self { ... }
}
Generating data for tests
Code
struct FooTests {
func BarTest() {
let names = ["Bob", "John", "Sue"]
let memberBuilder = Builder<Member>()
.with(\.name, value: names.randomElement()!)
.with(\.userId, value: UUID())
let member = memberBuilder.build()
...
}
}
struct Builder<Type> {
func with<Prop>(_ property: KeyPath<Type, Prop>, value: @autoclosure () -> Prop) -> Self { ... }
func build() -> Type { ... }
}