Latest 1.4.2
Homepage https://github.com/vincent-pradeilles/KeyPathKit
License MIT
Platforms ios 9.0, tvos 10.0, osx 10.12, watchos 3.0
Frameworks Foundation
Authors

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

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

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.

Latest podspec

{
    "name": "KeyPathKit",
    "version": "1.4.2",
    "summary": "KeyPathKit leverages Swift 4 KeyPath type in order to implement a SQL-like data manipulation API",
    "description": "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:nnlet string = "Foo"nlet keyPathForCount = String.countnnlet count = string[keyPath: keyPathForCount] // count == 3nThe great part is that the syntax can be very concise, because it supports type inference and property chaining.nnConsequently, 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.nnSQL 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.nnBut what really stands KeyPathKit appart from the competition is its clever syntax that allows to express queries in a very seamless fashion. For instance :nncontacts.filter(where: .lastName == "Webb" && .age < 40)",
    "homepage": "https://github.com/vincent-pradeilles/KeyPathKit",
    "license": {
        "type": "MIT",
        "file": "LICENSE"
    },
    "authors": {
        "Vincent Pradeilles": "[email protected]"
    },
    "source": {
        "git": "https://github.com/vincent-pradeilles/KeyPathKit.git",
        "tag": "1.4.2"
    },
    "platforms": {
        "ios": "9.0",
        "tvos": "10.0",
        "osx": "10.12",
        "watchos": "3.0"
    },
    "frameworks": "Foundation",
    "source_files": "Sources/**/*.swift"
}

Pin It on Pinterest

Share This