From 834065ed6f3c4cc396fd3188f4583f625f828b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fai=C3=A7al=20Tchirou?= Date: Sun, 3 Mar 2024 12:54:44 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20comparing=20r?= =?UTF-8?q?aw=20representables=20(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PredicateKit.podspec | 2 +- PredicateKit.xcodeproj/project.pbxproj | 8 +- .../CoreData/NSFetchRequestBuilder.swift | 17 +- PredicateKit/Predicate.swift | 21 +++ PredicateKit/Primitive.swift | 11 ++ .../NSFetchRequestBuilderTests.swift | 146 ++++++++++++++++++ ...SManagedObjectContextExtensionsTests.swift | 40 +++++ .../DataModel.xcdatamodel/contents | 3 +- README.md | 15 +- 9 files changed, 243 insertions(+), 20 deletions(-) diff --git a/PredicateKit.podspec b/PredicateKit.podspec index 385dd21..c3252ee 100644 --- a/PredicateKit.podspec +++ b/PredicateKit.podspec @@ -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. diff --git a/PredicateKit.xcodeproj/project.pbxproj b/PredicateKit.xcodeproj/project.pbxproj index 26af6ed..6481210 100644 --- a/PredicateKit.xcodeproj/project.pbxproj +++ b/PredicateKit.xcodeproj/project.pbxproj @@ -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 */; }; @@ -59,6 +58,8 @@ 3203472C254C952500F9661B /* NSFetchRequestInspector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFetchRequestInspector.swift; sourceTree = ""; }; 32034734254CC05300F9661B /* MockNSFetchRequestInspector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNSFetchRequestInspector.swift; sourceTree = ""; }; 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 = ""; }; + 32B8E6572B948E7900512327 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 32C8F72225B22CBE00903E22 /* SwiftUISupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUISupport.swift; sourceTree = ""; }; 32C8F75425B248C700903E22 /* SwiftUISupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUISupportTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -89,6 +90,8 @@ 320346692546AA5400F9661B /* PredicateKitTests */, 3203465D2546AA5400F9661B /* Products */, 320347242549A66E00F9661B /* README.md */, + 32B8E6572B948E7900512327 /* Package.swift */, + 32B8E6552B948E5100512327 /* PredicateKit.podspec */, ); sourceTree = ""; }; @@ -282,7 +285,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 320347252549A66E00F9661B /* README.md in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -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 = ""; }; @@ -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 = ""; }; diff --git a/PredicateKit/CoreData/NSFetchRequestBuilder.swift b/PredicateKit/CoreData/NSFetchRequestBuilder.swift index ba9d442..907ccab 100644 --- a/PredicateKit/CoreData/NSFetchRequestBuilder.swift +++ b/PredicateKit/CoreData/NSFetchRequestBuilder.swift @@ -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) @@ -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 { @@ -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 { diff --git a/PredicateKit/Predicate.swift b/PredicateKit/Predicate.swift index 9109d55..610eb98 100644 --- a/PredicateKit/Predicate.swift +++ b/PredicateKit/Predicate.swift @@ -371,14 +371,27 @@ public func < (lhs: E, rhs: T) -> Pre .comparison(.init(lhs, .lessThan, rhs)) } +public func < (lhs: E, rhs: T) -> Predicate where E.Value == T, T.RawValue: Comparable & Primitive { + .comparison(.init(lhs, .lessThan, rhs.rawValue)) +} + public func <= (lhs: E, rhs: T) -> Predicate where E.Value == T { .comparison(.init(lhs, .lessThanOrEqual, rhs)) } +public func <= (lhs: E, rhs: T) -> Predicate where E.Value == T, T.RawValue: Comparable & Primitive { + .comparison(.init(lhs, .lessThanOrEqual, rhs.rawValue)) +} + + public func == (lhs: E, rhs: T) -> Predicate where E.Value == T { .comparison(.init(lhs, .equal, rhs)) } +public func == (lhs: E, rhs: T) -> Predicate 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 == (lhs: E, rhs: T) -> Predicate where E.Value == T, T.ID: Primitive { .comparison(.init(ObjectIdentifier(root: lhs), .equal, rhs.id)) @@ -397,10 +410,18 @@ public func >= (lhs: E, rhs: T) -> Pr .comparison(.init(lhs, .greaterThanOrEqual, rhs)) } +public func >= (lhs: E, rhs: T) -> Predicate where E.Value == T, T.RawValue: Comparable & Primitive { + .comparison(.init(lhs, .greaterThanOrEqual, rhs.rawValue)) +} + public func > (lhs: E, rhs: T) -> Predicate where E.Value == T { .comparison(.init(lhs, .greaterThan, rhs)) } +public func > (lhs: E, rhs: T) -> Predicate where E.Value == T, T.RawValue: Comparable & Primitive { + .comparison(.init(lhs, .greaterThan, rhs.rawValue)) +} + // MARK: - Compound Predicates public func && (lhs: Predicate, rhs: Predicate) -> Predicate { diff --git a/PredicateKit/Primitive.swift b/PredicateKit/Primitive.swift index 08ac690..9bab885 100644 --- a/PredicateKit/Primitive.swift +++ b/PredicateKit/Primitive.swift @@ -24,6 +24,7 @@ import Foundation public protocol Primitive { static var type: Type { get } + var predicateValue: Any? { get } } public indirect enum Type: Equatable { @@ -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 } } @@ -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 { @@ -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: ()) { } } diff --git a/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift b/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift index 1bb2438..b3dc8b7 100644 --- a/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift +++ b/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift @@ -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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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() @@ -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 { @@ -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( diff --git a/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift b/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift index e8e1131..3535524 100644 --- a/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift +++ b/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift @@ -137,6 +137,22 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase { XCTAssertEqual(notes.first?.numberOfViews, 42) } + func testFetchWithEnumComparison() throws { + try container.viewContext.insertNotes( + (text: "Hello, World!", creationDate: Date(), numberOfViews: 42, tags: ["greeting"], type: .freeForm), + (text: "Goodbye!", creationDate: Date(), numberOfViews: 122, tags: ["greeting"], type: .structured) + ) + + let notes: [Note] = try container.viewContext + .fetch(where: \Note.type == .freeForm) + .result() + + XCTAssertEqual(notes.count, 1) + XCTAssertEqual(notes.first?.text, "Hello, World!") + XCTAssertEqual(notes.first?.tags, ["greeting"]) + XCTAssertEqual(notes.first?.numberOfViews, 42) + } + func testFetchAll() throws { try container.viewContext.insertNotes( (text: "Hello, World!", creationDate: Date(), numberOfViews: 42, tags: ["greeting"]), @@ -697,6 +713,7 @@ class Note: NSManagedObject { @NSManaged var numberOfViews: Int @NSManaged var tags: [String] @NSManaged var attachment: Attachment + @NSManaged var type: NoteType } class Account: NSManagedObject { @@ -727,6 +744,11 @@ class Attachment: NSManagedObject, Identifiable { @NSManaged var id: String } +@objc enum NoteType: Int { + case freeForm + case structured +} + // MARK: - extension XCTestCase { @@ -758,6 +780,7 @@ private extension NSManagedObjectContext { note.tags = description.tags note.numberOfViews = description.numberOfViews note.creationDate = description.creationDate + note.type = .freeForm } try save() @@ -773,6 +796,7 @@ private extension NSManagedObjectContext { note.numberOfViews = description.numberOfViews note.creationDate = description.creationDate note.updateDate = description.updateDate + note.type = .freeForm } try save() @@ -787,6 +811,7 @@ private extension NSManagedObjectContext { note.tags = description.tags note.numberOfViews = description.numberOfViews note.creationDate = description.creationDate + note.type = .freeForm if let attachment = description.attachment { note.attachment = attachment @@ -796,6 +821,21 @@ private extension NSManagedObjectContext { try save() } + func insertNotes( + _ notes: (text: String, creationDate: Date, numberOfViews: Int, tags: [String], type: NoteType)... + ) throws { + for description in notes { + let note = NSEntityDescription.insertNewObject(forEntityName: "Note", into: self) as! Note + note.text = description.text + note.tags = description.tags + note.numberOfViews = description.numberOfViews + note.creationDate = description.creationDate + note.type = description.type + } + + try save() + } + func insertAccounts(purchases: [[Double]]) throws { for description in purchases { let account = NSEntityDescription.insertNewObject(forEntityName: "Account", into: self) as! Account diff --git a/PredicateKitTests/Resources/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents b/PredicateKitTests/Resources/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents index c11eeb7..20042af 100644 --- a/PredicateKitTests/Resources/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents +++ b/PredicateKitTests/Resources/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -18,6 +18,7 @@ + diff --git a/README.md b/README.md index 0beca34..8ba11b5 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,16 @@ class Note: NSManagedObject { @NSManaged var numberOfViews: Int @NSManaged var tags: [String] @NSManaged var attachment: Attachment + @NSManaged var type: NoteType +} + +class Attachment: NSManagedObject, Identifiable { + // ... +} + +@objc enum NoteType: Int { + case freeForm + // ... } // Matches all notes where the text is equal to "Hello, World!". @@ -289,8 +299,11 @@ let predicate = \Note.creationDate < Date() // Matches all notes where the number of views is at least 120. let predicate = \Note.numberOfViews >= 120 -// Matches all notes having the specified attachment. `Attachment` must conform to `Identifiable`. +// Matches all notes having the specified attachment (`Attachment` must conform to `Identifiable`). let predicate = \Note.attachment == attachment + +// Matches all free form notes (assuming `NoteType` is an enumeration whose `RawValue` conforms to `Equatable`). +let predicate = \Note.type == .freeForm ``` #### String comparisons