Skip to content

Commit

Permalink
Merge pull request #3 from PureSwift/feature/evaluation
Browse files Browse the repository at this point in the history
Added Predicate evaluation engine
  • Loading branch information
colemancda authored Apr 14, 2020
2 parents db66380 + 3ebe982 commit 1b07ced
Show file tree
Hide file tree
Showing 20 changed files with 2,304 additions and 74 deletions.
5 changes: 2 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:4.1
// swift-tools-version:5.0
import PackageDescription

let package = Package(
Expand All @@ -22,6 +22,5 @@ let package = Package(
"Predicate"
]
)
],
swiftLanguageVersions: [4]
]
)
56 changes: 44 additions & 12 deletions Sources/Comparison.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,50 @@ public extension Comparison {

enum Operator: String, Codable {

/// A less-than predicate.
case lessThan = "<"

/// A less-than-or-equal-to predicate.
case lessThanOrEqualTo = "<="

/// A greater-than predicate.
case greaterThan = ">"

/// A greater-than-or-equal-to predicate.
case greaterThanOrEqualTo = ">="

/// An equal-to predicate.
case equalTo = "="

/// A not-equal-to predicate.
case notEqualTo = "!="

/// A full regular expression matching predicate.
case matches = "MATCHES"

/// A simple subset of the MATCHES predicate, similar in behavior to SQL LIKE.
case like = "LIKE"

/// A begins-with predicate.
case beginsWith = "BEGINSWITH"

/// An ends-with predicate.
case endsWith = "ENDSWITH"

/// A predicate to determine if the left hand side is in the right hand side.
///
/// - Note: For strings, returns true if the left hand side is a substring of the right hand side .
/// For collections, returns true if the left hand side is in the right hand side .
case `in` = "IN"

/// A predicate to determine if the left hand side contains the right hand side.
///
/// Returns` true if [lhs contains rhs];` the left hand side must be an `Expression` that evaluates to a collection or string.
case contains = "CONTAINS"

/// A predicate to determine if the left hand side lies at or between bounds specified by the right hand side.
///
/// - Note: Returns `true if [lhs between rhs];` the right hand side must be an array in which the first element sets the lower bound and the second element the upper, inclusive. Comparison is performed using compare(_:) or the class-appropriate equivalent.
case between = "BETWEEN"
}
}
Expand Down Expand Up @@ -162,7 +194,7 @@ public func != (lhs: Expression, rhs: Expression) -> Predicate {

public func < <T: PredicateValue>(lhs: String, rhs: T) -> Predicate {

let comparison = Comparison(left: .keyPath(lhs),
let comparison = Comparison(left: .keyPath(.init(rawValue: lhs)),
right: .value(rhs.predicateValue),
type: .lessThan)

Expand All @@ -171,7 +203,7 @@ public func < <T: PredicateValue>(lhs: String, rhs: T) -> Predicate {

public func <= <T: PredicateValue>(lhs: String, rhs: T) -> Predicate {

let comparison = Comparison(left: .keyPath(lhs),
let comparison = Comparison(left: .keyPath(.init(rawValue: lhs)),
right: .value(rhs.predicateValue),
type: .lessThanOrEqualTo)

Expand All @@ -180,7 +212,7 @@ public func <= <T: PredicateValue>(lhs: String, rhs: T) -> Predicate {

public func > <T: PredicateValue>(lhs: String, rhs: T) -> Predicate {

let comparison = Comparison(left: .keyPath(lhs),
let comparison = Comparison(left: .keyPath(.init(rawValue: lhs)),
right: .value(rhs.predicateValue),
type: .greaterThan)

Expand All @@ -189,7 +221,7 @@ public func > <T: PredicateValue>(lhs: String, rhs: T) -> Predicate {

public func >= <T: PredicateValue> (lhs: String, rhs: T) -> Predicate {

let comparison = Comparison(left: .keyPath(lhs),
let comparison = Comparison(left: .keyPath(.init(rawValue: lhs)),
right: .value(rhs.predicateValue),
type: .greaterThanOrEqualTo)

Expand All @@ -198,7 +230,7 @@ public func >= <T: PredicateValue> (lhs: String, rhs: T) -> Predicate {

public func == <T: PredicateValue> (lhs: String, rhs: T) -> Predicate {

let comparison = Comparison(left: .keyPath(lhs),
let comparison = Comparison(left: .keyPath(.init(rawValue: lhs)),
right: .value(rhs.predicateValue),
type: .equalTo)

Expand All @@ -207,7 +239,7 @@ public func == <T: PredicateValue> (lhs: String, rhs: T) -> Predicate {

public func != <T: PredicateValue> (lhs: String, rhs: T) -> Predicate {

let comparison = Comparison(left: .keyPath(lhs),
let comparison = Comparison(left: .keyPath(.init(rawValue: lhs)),
right: .value(rhs.predicateValue),
type: .notEqualTo)

Expand All @@ -219,21 +251,21 @@ public extension String {

func compare(_ type: Comparison.Operator, _ rhs: Expression) -> Predicate {

let comparison = Comparison(left: .keyPath(self), right: rhs, type: type)
let comparison = Comparison(left: .keyPath(.init(rawValue: self)), right: rhs, type: type)

return .comparison(comparison)
}

func compare(_ type: Comparison.Operator, _ options: Set<Comparison.Option>, _ rhs: Expression) -> Predicate {

let comparison = Comparison(left: .keyPath(self), right: rhs, type: type, options: options)
let comparison = Comparison(left: .keyPath(.init(rawValue: self)), right: rhs, type: type, options: options)

return .comparison(comparison)
}

func compare(_ modifier: Comparison.Modifier, _ type: Comparison.Operator, _ options: Set<Comparison.Option>, _ rhs: Expression) -> Predicate {

let comparison = Comparison(left: .keyPath(self), right: rhs, type: type, modifier: modifier, options: options)
let comparison = Comparison(left: .keyPath(.init(rawValue: self)), right: rhs, type: type, modifier: modifier, options: options)

return .comparison(comparison)
}
Expand All @@ -244,7 +276,7 @@ public extension String {

let rightExpression = Expression.value(.collection(values))

let comparison = Comparison(left: .keyPath(self), right: rightExpression, type: .`in`, modifier: .any)
let comparison = Comparison(left: .keyPath(.init(rawValue: self)), right: rightExpression, type: .`in`, modifier: .any)

return .comparison(comparison)
}
Expand All @@ -255,7 +287,7 @@ public extension String {

let rightExpression = Expression.value(.collection(values))

let comparison = Comparison(left: .keyPath(self), right: rightExpression, type: .`in`, modifier: .all)
let comparison = Comparison(left: .keyPath(.init(rawValue: self)), right: rightExpression, type: .`in`, modifier: .all)

return .comparison(comparison)
}
Expand All @@ -266,7 +298,7 @@ public extension String {

let rightExpression = Expression.value(.collection(values))

let comparison = Comparison(left: .keyPath(self), right: rightExpression, type: .`in`, options: options)
let comparison = Comparison(left: .keyPath(.init(rawValue: self)), right: rightExpression, type: .`in`, options: options)

return .comparison(comparison)
}
Expand Down
109 changes: 109 additions & 0 deletions Sources/Context.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// File.swift
//
//
// Created by Alsey Coleman Miller on 4/13/20.
//

import Foundation

/// Context for evaluating predicates.
public struct PredicateContext: Equatable, Hashable {

public typealias KeyPath = PredicateKeyPath

public var locale: Locale?

public var values: [KeyPath: Value]

public init(values: [KeyPath: Value],
locale: Locale? = nil) {

self.values = values
self.locale = locale
}

public subscript(keyPath: KeyPath) -> Value? {
return values[keyPath]
}
}

internal extension PredicateContext {

func value(for expression: Expression) throws -> Value {
switch expression {
case let .value(value):
return value
case let .keyPath(keyPath):
if let value = self[keyPath] {
return value
} else {
// try to find collection
if let values = collection(for: keyPath) {
return .collection(values)
} else {
throw PredicateError.invalidKeyPath(keyPath)
}
}
}
}
}

private extension PredicateContext {

func collection(for keyPath: PredicateKeyPath) -> [Value]? {
var remainderKeys = [PredicateKeyPath.Key]()
var keyPath = keyPath
repeat {
// try again with smaller keyPath
defer {
let lastKey = keyPath.removeLast()
remainderKeys.append(lastKey)
}
let countPath = keyPath.appending(.operator(.count))
guard let countValue = self[countPath],
let count = NSNumber(value: countValue)?.uintValue
else { continue }
let values = (0 ..< count)
.map { keyPath.appending(.index($0)).appending(contentsOf: remainderKeys) }
.compactMap { self[$0] }
guard values.count == Int(count)
else { return nil }
return values
} while keyPath.keys.isEmpty == false
return nil
}
}

// MARK: - ExpressibleByDictionaryLiteral

extension PredicateContext: ExpressibleByDictionaryLiteral {

public init(dictionaryLiteral elements: (PredicateKeyPath, Value)...) {
self.init(values: [PredicateKeyPath: Value](uniqueKeysWithValues: elements))
}
}

// MARK: - PredicateEvaluatable

extension PredicateContext: PredicateEvaluatable {

public func evaluate(with predicate: Predicate) throws -> Bool {

switch predicate {
case let .value(value):
return value
case let .comparison(comparison):
return try evaluate(with: comparison)
case let .compound(compound):
return try evaluate(with: compound)
}
}

internal func evaluate(with predicate: Comparison) throws -> Bool {

let lhs = try value(for: predicate.left)
let rhs = try value(for: predicate.right)
return try lhs.compare(rhs, operator: predicate.type, modifier: predicate.modifier, options: predicate.options)
}
}
Loading

0 comments on commit 1b07ced

Please sign in to comment.