Skip to content

Commit

Permalink
✨ Add support for comparing raw representables (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
ftchirou authored Mar 3, 2024
1 parent 3bb2639 commit 834065e
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 20 deletions.
2 changes: 1 addition & 1 deletion PredicateKit.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

Pod::Spec.new do |spec|
spec.name = "PredicateKit"
spec.version = "1.8.0"
spec.version = "1.9.0"
spec.summary = "Write expressive and type-safe predicates for CoreData using key-paths, comparisons and logical operators, literal values, and functions."
spec.description = <<-DESC
PredicateKit allows Swift developers to write expressive and type-safe predicates for CoreData using key-paths, comparisons and logical operators, literal values, and functions.
Expand Down
8 changes: 6 additions & 2 deletions PredicateKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
3203470F2548C54200F9661B /* DataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 3203470D2548C54200F9661B /* DataModel.xcdatamodeld */; };
320347132548EE4B00F9661B /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320347122548EE4B00F9661B /* Functions.swift */; };
32034719254903B700F9661B /* XCTestCaseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32034718254903B700F9661B /* XCTestCaseExtensions.swift */; };
320347252549A66E00F9661B /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 320347242549A66E00F9661B /* README.md */; };
3203472D254C952600F9661B /* NSFetchRequestInspector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3203472C254C952500F9661B /* NSFetchRequestInspector.swift */; };
32034735254CC05300F9661B /* MockNSFetchRequestInspector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32034734254CC05300F9661B /* MockNSFetchRequestInspector.swift */; };
322517D82AEF7E85003DD2CD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 322517D72AEF7E85003DD2CD /* PrivacyInfo.xcprivacy */; };
Expand Down Expand Up @@ -59,6 +58,8 @@
3203472C254C952500F9661B /* NSFetchRequestInspector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFetchRequestInspector.swift; sourceTree = "<group>"; };
32034734254CC05300F9661B /* MockNSFetchRequestInspector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNSFetchRequestInspector.swift; sourceTree = "<group>"; };
322517D72AEF7E85003DD2CD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = PredicateKit/PrivacyInfo.xcprivacy; sourceTree = SOURCE_ROOT; };
32B8E6552B948E5100512327 /* PredicateKit.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = PredicateKit.podspec; sourceTree = "<group>"; };
32B8E6572B948E7900512327 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
32C8F72225B22CBE00903E22 /* SwiftUISupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUISupport.swift; sourceTree = "<group>"; };
32C8F75425B248C700903E22 /* SwiftUISupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUISupportTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -89,6 +90,8 @@
320346692546AA5400F9661B /* PredicateKitTests */,
3203465D2546AA5400F9661B /* Products */,
320347242549A66E00F9661B /* README.md */,
32B8E6572B948E7900512327 /* Package.swift */,
32B8E6552B948E5100512327 /* PredicateKit.podspec */,
);
sourceTree = "<group>";
};
Expand Down Expand Up @@ -282,7 +285,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
320347252549A66E00F9661B /* README.md in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -388,6 +390,7 @@
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
Expand Down Expand Up @@ -445,6 +448,7 @@
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
Expand Down
17 changes: 2 additions & 15 deletions PredicateKit/CoreData/NSFetchRequestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ struct NSFetchRequestBuilder {
case .none:
return NSCompoundPredicate(notPredicateWithSubpredicate: NSComparisonPredicate(
leftExpression: makeExpression(from: comparison.expression),
rightExpression: NSExpression(forConstantValue: comparison.value),
rightExpression: makeExpression(from: comparison.value),
modifier: makeComparisonModifier(from: comparison.modifier),
type: makeOperator(from: comparison.operator),
options: makeComparisonOptions(from: comparison.options)
Expand Down Expand Up @@ -111,7 +111,7 @@ struct NSFetchRequestBuilder {
}

private func makeExpression(from primitive: Primitive) -> NSExpression {
return NSExpression(forConstantValue: primitive.value)
return NSExpression(forConstantValue: primitive.predicateValue)
}

private func makeOperator(from operator: ComparisonOperator) -> NSComparisonPredicate.Operator {
Expand Down Expand Up @@ -309,19 +309,6 @@ extension ObjectIdentifier: NSExpressionConvertible where Object: NSExpressionCo
}
}

// MARK: - Primitive

private extension Primitive {
var value: Any? {
switch Self.type {
case .nil:
return NSNull()
default:
return self
}
}
}

// MARK: - KeyPath

extension AnyKeyPath {
Expand Down
21 changes: 21 additions & 0 deletions PredicateKit/Predicate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,14 +371,27 @@ public func < <E: Expression, T: Comparable & Primitive> (lhs: E, rhs: T) -> Pre
.comparison(.init(lhs, .lessThan, rhs))
}

public func < <E: Expression, T: RawRepresentable> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T, T.RawValue: Comparable & Primitive {
.comparison(.init(lhs, .lessThan, rhs.rawValue))
}

public func <= <E: Expression, T: Comparable & Primitive> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T {
.comparison(.init(lhs, .lessThanOrEqual, rhs))
}

public func <= <E: Expression, T: RawRepresentable> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T, T.RawValue: Comparable & Primitive {
.comparison(.init(lhs, .lessThanOrEqual, rhs.rawValue))
}


public func == <E: Expression, T: Equatable & Primitive> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T {
.comparison(.init(lhs, .equal, rhs))
}

public func == <E: Expression, T: RawRepresentable> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T, T.RawValue: Equatable & Primitive {
.comparison(.init(lhs, .equal, rhs.rawValue))
}

@available(iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public func == <E: Expression, T: Identifiable> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T, T.ID: Primitive {
.comparison(.init(ObjectIdentifier<E, T.ID>(root: lhs), .equal, rhs.id))
Expand All @@ -397,10 +410,18 @@ public func >= <E: Expression, T: Comparable & Primitive> (lhs: E, rhs: T) -> Pr
.comparison(.init(lhs, .greaterThanOrEqual, rhs))
}

public func >= <E: Expression, T: RawRepresentable> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T, T.RawValue: Comparable & Primitive {
.comparison(.init(lhs, .greaterThanOrEqual, rhs.rawValue))
}

public func > <E: Expression, T: Comparable & Primitive> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T {
.comparison(.init(lhs, .greaterThan, rhs))
}

public func > <E: Expression, T: RawRepresentable> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T, T.RawValue: Comparable & Primitive {
.comparison(.init(lhs, .greaterThan, rhs.rawValue))
}

// MARK: - Compound Predicates

public func && <T> (lhs: Predicate<T>, rhs: Predicate<T>) -> Predicate<T> {
Expand Down
11 changes: 11 additions & 0 deletions PredicateKit/Primitive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import Foundation

public protocol Primitive {
static var type: Type { get }
var predicateValue: Any? { get }
}

public indirect enum Type: Equatable {
Expand All @@ -50,6 +51,10 @@ public indirect enum Type: Equatable {
case `nil`
}

extension Primitive {
public var predicateValue: Any? { self }
}

extension Bool: Primitive {
public static var type: Type { .bool }
}
Expand Down Expand Up @@ -110,8 +115,12 @@ extension Date: Primitive {
public static var type: Type { .date }
}

// TODO: Potentially remove this in the next major version. RawRepresentables (with primitive
// raw values) can already be used in predicates without explicitly conforming to Primitive.
extension Primitive where Self: RawRepresentable, RawValue: Primitive {
public static var type: Type { RawValue.type }

public var predicateValue: Any? { rawValue }
}

extension Array: Primitive where Element: Primitive {
Expand All @@ -137,6 +146,8 @@ extension Optional: Primitive where Wrapped: Primitive {
public struct Nil: Primitive, ExpressibleByNilLiteral {
public static var type: Type { .nil }

public var predicateValue: Any? { NSNull() }

public init(nilLiteral: ()) {
}
}
Expand Down
146 changes: 146 additions & 0 deletions PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,136 @@ final class NSFetchRequestBuilderTests: XCTestCase {
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testEqualityWithRawRepresentable() throws {
let request = makeRequest(\Data.dataType == .two)
let builder = makeRequestBuilder()

let result: NSFetchRequest<Data> = builder.makeRequest(from: request)

let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate)
XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "dataType"))
XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: DataType.two.rawValue))
XCTAssertEqual(comparison.predicateOperatorType, .equalTo)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testLessThanWithRawRepresentable() throws {
let request = makeRequest(\Data.dataType < .two)
let builder = makeRequestBuilder()

let result: NSFetchRequest<Data> = builder.makeRequest(from: request)

let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate)
XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "dataType"))
XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: DataType.two.rawValue))
XCTAssertEqual(comparison.predicateOperatorType, .lessThan)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testLessThanOrEqualWithRawRepresentable() throws {
let request = makeRequest(\Data.dataType <= .two)
let builder = makeRequestBuilder()

let result: NSFetchRequest<Data> = builder.makeRequest(from: request)

let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate)
XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "dataType"))
XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: DataType.two.rawValue))
XCTAssertEqual(comparison.predicateOperatorType, .lessThanOrEqualTo)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testGreaterThanWithRawRepresentable() throws {
let request = makeRequest(\Data.dataType > .two)
let builder = makeRequestBuilder()

let result: NSFetchRequest<Data> = builder.makeRequest(from: request)

let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate)
XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "dataType"))
XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: DataType.two.rawValue))
XCTAssertEqual(comparison.predicateOperatorType, .greaterThan)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testGreaterThanOrEqualWithRawRepresentable() throws {
let request = makeRequest(\Data.dataType >= .two)
let builder = makeRequestBuilder()

let result: NSFetchRequest<Data> = builder.makeRequest(from: request)

let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate)
XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "dataType"))
XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: DataType.two.rawValue))
XCTAssertEqual(comparison.predicateOperatorType, .greaterThanOrEqualTo)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testEqualityWithRawRepresentableConformingToPrimitive() throws {
let request = makeRequest(\Data.primitiveDataType == .three)
let builder = makeRequestBuilder()

let result: NSFetchRequest<Data> = builder.makeRequest(from: request)

let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate)
XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "primitiveDataType"))
XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: PrimitiveDataType.three.rawValue))
XCTAssertEqual(comparison.predicateOperatorType, .equalTo)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testLessThanWithRawRepresentableConformingToPrimitive() throws {
let request = makeRequest(\Data.primitiveDataType < .two)
let builder = makeRequestBuilder()

let result: NSFetchRequest<Data> = builder.makeRequest(from: request)

let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate)
XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "primitiveDataType"))
XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: PrimitiveDataType.two.rawValue))
XCTAssertEqual(comparison.predicateOperatorType, .lessThan)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testLessThanOrEqualWithRawRepresentableConformingToPrimitive() throws {
let request = makeRequest(\Data.primitiveDataType <= .two)
let builder = makeRequestBuilder()

let result: NSFetchRequest<Data> = builder.makeRequest(from: request)

let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate)
XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "primitiveDataType"))
XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: PrimitiveDataType.two.rawValue))
XCTAssertEqual(comparison.predicateOperatorType, .lessThanOrEqualTo)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testGreaterThanWithRawRepresentableConformingToPrimitive() throws {
let request = makeRequest(\Data.primitiveDataType > .two)
let builder = makeRequestBuilder()

let result: NSFetchRequest<Data> = builder.makeRequest(from: request)

let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate)
XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "primitiveDataType"))
XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: PrimitiveDataType.two.rawValue))
XCTAssertEqual(comparison.predicateOperatorType, .greaterThan)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testGreaterThanOrEqualWithRawRepresentableConformingToPrimitive() throws {
let request = makeRequest(\Data.primitiveDataType >= .two)
let builder = makeRequestBuilder()

let result: NSFetchRequest<Data> = builder.makeRequest(from: request)

let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate)
XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "primitiveDataType"))
XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: PrimitiveDataType.two.rawValue))
XCTAssertEqual(comparison.predicateOperatorType, .greaterThanOrEqualTo)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testArrayElementEqualPredicate() throws {
let request = makeRequest((\Data.relationships).last(\.count) == 42)
let builder = makeRequestBuilder()
Expand Down Expand Up @@ -1156,6 +1286,8 @@ private class Data: NSManagedObject {
@NSManaged var optionalRelationships: [Relationship]?
@NSManaged var identifiable: IdentifiableData
@NSManaged var optionalIdentifiable: IdentifiableData?
@NSManaged var dataType: DataType
@NSManaged var primitiveDataType: PrimitiveDataType
}

private class Relationship: NSManagedObject {
Expand All @@ -1164,6 +1296,20 @@ private class Relationship: NSManagedObject {
@NSManaged var count: Int
}

@objc private enum DataType: Int {
case zero
case one
case two
case three
}

@objc private enum PrimitiveDataType: Int, Primitive {
case zero
case one
case two
case three
}

private class DataStore: NSAtomicStore {
init(_ value: Bool = false) {
super.init(
Expand Down
Loading

0 comments on commit 834065e

Please sign in to comment.