Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/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.
https://github.com/vincent-pradeilles/KeyPathKit

data sql swift

Last synced: 3 months ago
JSON representation

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.

Awesome Lists containing this project

README

        

# KeyPathKit

[![Build Status](https://travis-ci.org/vincent-pradeilles/KeyPathKit.svg?branch=master)](https://travis-ci.org/vincent-pradeilles/KeyPathKit)
![platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20macOS%20%7C%20tvOS%20%7C%20watchOS-333333.svg)
![pod](https://img.shields.io/cocoapods/v/KeyPathKit.svg)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager)

## 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:

```swift
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 :

```swift
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
// 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

* [and](#and)
* [average](#average)
* [between](#between)
* [contains](#contains)
* [distinct](#distinct)
* [drop](#drop)
* [filter](#filter)
* [filterIn](#filterin)
* [filterLess](#filterless)
* [filterLike](#filterlike)
* [filterMore](#filtermore)
* [first](#first)
* [groupBy](#groupby)
* [join](#join)
* [map](#map)
* [mapTo](#mapto)
* [max](#max)
* [min](#min)
* [or](#or)
* [patternMatching](#patternMatching)
* [prefix](#prefix)
* [sum](#sum)
* [sort](#sort)

## Operator details

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

```swift
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`.

```swift
contacts.and(\.hasDriverLicense)
contacts.and(\.isAmerican)
```

```
false
true
```

### average

Calculates the average of a numerical property.

```swift
contacts.average(of: \.age).rounded()
```

```
25
```

### between

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

```swift
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.

```swift
contacts.contains(where: \.hasDriverLicense)
contacts.contains(where: \.lastName.count > 10)
```

```
true
false
```

### distinct

Returns all the distinct values for the property.

```swift
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.

```swift
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.

```swift
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:

```swift
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`.

```swift
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.

```swift
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)]
```

```swift
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.

```swift
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.

```swift
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)]
```

```swift
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.

```swift
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.

```swift
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.

```swift
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:

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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`.

```swift
contacts.or(\.hasDriverLicense)
```

```
true
```

### patternMatching

Allows the use of predicates inside a `switch` statement:

```swift
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.

```swift
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.

```swift
contacts.sum(of: \.age)
```

```
177
```

### sort

Sorts the elements with respect to a `Comparable` property.

```swift
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.

```swift
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

* Twitter: [@v_pradeilles](https://twitter.com/v_pradeilles)

## Thanks

A big thank you to JΓ©rΓ΄me Alves ([elegantswift.com](http://elegantswift.com)) for coming up with the right modelization to allow sorting on multiple properties with heterogenous type.