Logo

0x5a.live

for different kinds of informations and explorations.

GitHub - vincent-pradeilles/KeyPathKit: KeyPathKit is a library that provides the standard functions to manipulate data along with a call-syntax that relies on typed keypaths to make the call sites as short and clean as possible.

KeyPathKit is a library that provides the standard functions to manipulate data along with a call-syntax that relies on typed keypaths to make the call sites as short and clean as possible. - vince...

Visit SiteGitHub - vincent-pradeilles/KeyPathKit: KeyPathKit is a library that provides the standard functions to manipulate data along with a call-syntax that relies on typed keypaths to make the call sites as short and clean as possible.

GitHub - vincent-pradeilles/KeyPathKit: KeyPathKit is a library that provides the standard functions to manipulate data along with a call-syntax that relies on typed keypaths to make the call sites as short and clean as possible.

KeyPathKit is a library that provides the standard functions to manipulate data along with a call-syntax that relies on typed keypaths to make the call sites as short and clean as possible. - vince...

Powered by 0x5a.live 💗

KeyPathKit

Build Status platforms pod Carthage compatible Swift Package Manager compatible

Context

Swift 4 has introduced a new type called KeyPath, with allows to access the properties of an object with a very nice syntax. For instance:

let string = "Foo"
let keyPathForCount = \String.count

let count = string[keyPath: keyPathForCount] // count == 3

The great part is that the syntax can be very concise, because it supports type inference and property chaining.

Purpose of KeyPathKit

Consequently, I thought it would be nice to leverage this new concept in order to build an API that allows to perform data manipulation in a very declarative fashion.

SQL is a great language for such manipulations, so I took inspiration from it and implemented most of its standard operators in Swift 4 using KeyPath.

But what really stands KeyPathKit appart from the competition is its clever syntax that allows to express queries in a very seamless fashion. For instance :

contacts.filter(where: \.lastName == "Webb" && \.age < 40)

Installation

CocoaPods

Add the following to your Podfile:

pod "KeyPathKit"

Carthage

Add the following to your Cartfile:

github "vincent-pradeilles/KeyPathKit"

Swift Package Manager

Create a file Package.swift:

// swift-tools-version:4.0

import PackageDescription

let package = Package(
    name: "YourProject",
    dependencies: [
        .package(url: "https://github.com/vincent-pradeilles/KeyPathKit.git", "1.0.0" ..< "2.0.0")
    ],
    targets: [
        .target(name: "YourProject", dependencies: ["KeyPathKit"])
    ]
)

Operators

Operator details

For the purpose of demonstrating the usage of the operators, the following mock data is defined:

struct Person {
    let firstName: String
    let lastName: String
    let age: Int
    let hasDriverLicense: Bool
    let isAmerican: Bool
}

let contacts = [
    Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
    Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
    Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
    Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
    Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
    Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true),
    Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)
]

and

Performs a boolean AND operation on a property of type Bool.

contacts.and(\.hasDriverLicense)
contacts.and(\.isAmerican)
false
true

average

Calculates the average of a numerical property.

contacts.average(of: \.age).rounded()
25

between

Filters out elements whose value for the property is not within the range.

contacts.between(\.age, range: 20...30)
// or
contacts.filter(where: 20...30 ~= \.age)
[Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

contains

Returns whether the sequence contains one element for which the specified boolean property or predicate is true.

contacts.contains(where: \.hasDriverLicense)
contacts.contains(where: \.lastName.count > 10)
true
false

distinct

Returns all the distinct values for the property.

contacts.distinct(\.lastName)
["Webb", "Elexson", "Zunino", "Alexson"]

drop

Returns a subsequence by skipping elements while a property of type Bool or a predicate evaluates to true, and returning the remaining elements.

contacts.drop(while: \.age < 40)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

filter

Filters out elements whose value is false for one (or several) boolean property.

contacts.filter(where: \.hasDriverLicense)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

Filter also works with predicates:

contacts.filter(where: \.firstName == "Webb")
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
 Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)]

filterIn

Filters out elements whose value for an Equatable property is not in a given Sequence.

contacts.filter(where: \.firstName, in: ["Alex", "John"])
[Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)]

filterLess

Filters out elements whose value is greater than a constant for a Comparable property.

contacts.filter(where: \.age, lessThan: 30)
// or
contacts.filter(where: \.age < 30)
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)]
contacts.filter(where: \.age, lessOrEqual: 30)
// or
contacts.filter(where: \.age <= 30)
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

filterLike

Filters out elements whose value for a string property does not match a regular expression.

contacts.filter(where: \.lastName, like: "^[A-Za-z]*son$")
[Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

filterMore

Filters out elements whose value is lesser than a constant for a Comparable property.

contacts.filter(where: \.age, moreThan: 30)
// or
contacts.filter(where: \.age > 30)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true)]
contacts.filter(where: \.age, moreOrEqual: 30)
// or
contacts.filter(where: \.age >= 30)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

first

Returns the first element matching a predicate.

contacts.first(where: \.lastName == "Webb")
Optional(Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true))

groupBy

Groups values by equality on the property.

contacts.groupBy(\.lastName)
["Alexson": [Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true)], 
 "Webb": [Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true), Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)], 
 "Elexson": [Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)], 
 "Zunino": [Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true)]]

join

Joins values of two sequences in tuples by the equality on their respective property.

contacts.join(\.firstName, with: contacts, on: \.lastName)
// or
contacts.join(with: contacts, where: \.firstName == \.lastName)
[(Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true)), 
 (Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true)), 
 (Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true))]

Joining on more than one attribute is also supported:

contacts.join(with: contacts, .where(\.firstName, equals: \.lastName), .where(\.hasDriverLicense, equals: \.isAmerican))
// or
contacts.join(with: contacts, where: \.firstName == \.lastName, \.hasDriverLicense == \.isAmerican)

map

Maps elements to their values of the property.

contacts.map(\.lastName)
["Webb", "Elexson", "Webb", "Zunino", "Alexson", "Webb", "Elexson"]

mapTo

Maps a sequence of properties to a function. This is, for instance, useful to extract a subset of properties into a structured type.

struct ContactCellModel {
    let firstName: String
    let lastName: String
}

contacts.map(\.lastName, \.firstName, to: ContactCellModel.init)
[ContactCellModel(firstName: "Webb", lastName: "Charlie"), 
 ContactCellModel(firstName: "Elexson", lastName: "Alex"), 
 ContactCellModel(firstName: "Webb", lastName: "Charles"), 
 ContactCellModel(firstName: "Zunino", lastName: "Alex"), 
 ContactCellModel(firstName: "Alexson", lastName: "Alex"), 
 ContactCellModel(firstName: "Webb", lastName: "John"), 
 ContactCellModel(firstName: "Elexson", lastName: "Webb")]

max

Returns the element with the greatest value for a Comparable property.

contacts.max(by: \.age)
contacts.max(\.age)
Optional(Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true))
Optional(45)

min

Returns the element with the minimum value for a Comparable property.

contacts.min(by: \.age)
contacts.min(\.age)
Optional(Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true))
Optional(8)

or

Performs a boolean OR operation on an property of type Bool.

contacts.or(\.hasDriverLicense)
true

patternMatching

Allows the use of predicates inside a switch statement:

switch person {
case \.firstName == "Charlie":
    print("I'm Charlie!")
    fallthrough
case \.age < 18:
    print("I'm not an adult...")
    fallthrough
default:
    break
}

prefix

Returns a subsequence containing the initial, consecutive elements for whose a property of type Bool or a predicate evaluates to true.

contacts.prefix(while: \.age < 40)
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
 Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true)]

sum

Calculates the sum of the values for a numerical property.

contacts.sum(of: \.age)
177

sort

Sorts the elements with respect to a Comparable property.

contacts.sorted(by: \.age)
[Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true)]

It's also possible to specify the sorting order, to sort on multiple criteria, or to do both.

contacts.sorted(by: .ascending(\.lastName), .descending(\.age))
[Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true)]

Author

Thanks

A big thank you to Jérôme Alves (elegantswift.com) for coming up with the right modelization to allow sorting on multiple properties with heterogenous type.

Swift Resources

are all listed below.

Resources

listed to get explored on!!

Made with ❤️

to provide different kinds of informations and resources.