From 8ac5bd84af3470eb90753c08b5f89c3bfa316130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Faic=CC=A7al=20Tchirou?= Date: Sat, 13 May 2023 12:45:46 +0200 Subject: [PATCH] Add support for testing objects for equality --- .../CoreData/NSFetchRequestBuilder.swift | 7 +++ PredicateKit/Predicate.swift | 12 ++++ .../NSFetchRequestBuilderTests.swift | 45 +++++++++++++++ ...SManagedObjectContextExtensionsTests.swift | 55 ++++++++++++++++++- PredicateKitTests/OperatorTests.swift | 32 +++++++++++ .../DataModel.xcdatamodel/contents | 17 +++--- README.md | 4 ++ 7 files changed, 162 insertions(+), 10 deletions(-) diff --git a/PredicateKit/CoreData/NSFetchRequestBuilder.swift b/PredicateKit/CoreData/NSFetchRequestBuilder.swift index f31542a..b2daa98 100644 --- a/PredicateKit/CoreData/NSFetchRequestBuilder.swift +++ b/PredicateKit/CoreData/NSFetchRequestBuilder.swift @@ -302,6 +302,13 @@ extension Query: NSExpressionConvertible { } } +extension ObjectIdentifier: NSExpressionConvertible where Object: NSExpressionConvertible { + func toNSExpression(options: NSExpressionConversionOptions) -> NSExpression { + let root = self.root.toNSExpression(options: options) + return NSExpression(format: "\(root).id") + } +} + // MARK: - Primitive private extension Primitive { diff --git a/PredicateKit/Predicate.swift b/PredicateKit/Predicate.swift index 05e0f60..204f4d9 100644 --- a/PredicateKit/Predicate.swift +++ b/PredicateKit/Predicate.swift @@ -313,6 +313,13 @@ public struct ArrayElementKeyPath: Expression where Array: Express let elementKeyPath: AnyKeyPath } +public struct ObjectIdentifier: Expression { + public typealias Root = Object + public typealias Value = Identifier + + let root: Object +} + enum ComparisonOperator { case lessThan case lessThanOrEqual @@ -371,6 +378,11 @@ public func == (lhs: E, rhs: T) -> Pre .comparison(.init(lhs, .equal, rhs)) } +@available(iOS 13.0, *) +public func == (lhs: E, rhs: T) -> Predicate where E.Value == T, T.ID: Primitive { + .comparison(.init(ObjectIdentifier(root: lhs), .equal, rhs.id)) +} + @_disfavoredOverload public func == (lhs: E, rhs: Nil) -> Predicate where E.Value: OptionalType { .comparison(.init(lhs, .equal, rhs)) diff --git a/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift b/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift index 78ed06c..9a95f7d 100644 --- a/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift +++ b/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift @@ -87,6 +87,27 @@ final class NSFetchRequestBuilderTests: XCTestCase { XCTAssertEqual(comparison.comparisonPredicateModifier, .direct) } + @available(iOS 13.0, *) + func testEqualityWithIdentifiable() throws { + guard let identifiable = makeIdentifiable() else { + XCTFail("could not initialize IdentifiableData") + return + } + + identifiable.id = "42" + + let request = makeRequest(\Data.identifiable == identifiable) + let builder = makeRequestBuilder() + + let result: NSFetchRequest = builder.makeRequest(from: request) + + let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate) + XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "identifiable.id")) + XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: "42")) + XCTAssertEqual(comparison.predicateOperatorType, .equalTo) + XCTAssertEqual(comparison.comparisonPredicateModifier, .direct) + } + func testArrayElementEqualPredicate() throws { let request = makeRequest((\Data.relationships).last(\.count) == 42) let builder = makeRequestBuilder() @@ -1078,6 +1099,25 @@ final class NSFetchRequestBuilderTests: XCTestCase { XCTAssertEqual(comparison.predicateOperatorType, .equalTo) XCTAssertEqual(comparison.comparisonPredicateModifier, .direct) } + + private func makeIdentifiable() -> IdentifiableData? { + guard + let model = NSManagedObjectModel.mergedModel(from: [Bundle(for: NSFetchRequestBuilderTests.self)]) + else { + return nil + } + + let container = makePersistentContainer(with: model) + + guard let identifiable = NSEntityDescription.insertNewObject( + forEntityName: "IdentifiableData", + into: container.viewContext + ) as? IdentifiableData else { + return nil + } + + return identifiable + } } // MARK: - @@ -1092,6 +1132,7 @@ private class Data: NSManagedObject { @NSManaged var relationships: [Relationship] @NSManaged var optionalRelationship: Relationship? @NSManaged var optionalRelationships: [Relationship]? + @NSManaged var identifiable: IdentifiableData } private class Relationship: NSManagedObject { @@ -1111,6 +1152,10 @@ private class DataStore: NSAtomicStore { } } +class IdentifiableData: NSManagedObject, Identifiable { + @NSManaged var id: String +} + private func makeRequest(_ predicate: Predicate) -> FetchRequest { .init(context: NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType), predicate: predicate) } diff --git a/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift b/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift index b7fb98c..9881008 100644 --- a/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift +++ b/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift @@ -116,6 +116,27 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase { XCTAssertNil(texts.first?["creationDate"]) } + @available(iOS 13.0, *) + func testFetchWithObjectComparison() throws { + let attachment1 = try container.viewContext.insertAttachment("1") + let attachment2 = try container.viewContext.insertAttachment("2") + + try container.viewContext.insertNotes( + (text: "Hello, World!", creationDate: Date(), numberOfViews: 42, tags: ["greeting"], attachment: attachment1 ), + (text: "Goodbye!", creationDate: Date(), numberOfViews: 3, tags: ["greeting"], attachment: attachment2 ), + (text: "See ya!", creationDate: Date(), numberOfViews: 3, tags: ["greeting"], attachment: attachment2 ) + ) + + let notes: [Note] = try container.viewContext + .fetch(where: \Note.attachment == attachment1) + .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"]), @@ -675,6 +696,7 @@ class Note: NSManagedObject { @NSManaged var updateDate: Date? @NSManaged var numberOfViews: Int @NSManaged var tags: [String] + @NSManaged var attachment: Attachment } class Account: NSManagedObject { @@ -701,9 +723,13 @@ class Profile: NSManagedObject { @NSManaged var creationDate: Date } +class Attachment: NSManagedObject, Identifiable { + @NSManaged var id: String +} + // MARK: - -private extension XCTestCase { +extension XCTestCase { func makePersistentContainer(with model: NSManagedObjectModel) -> NSPersistentContainer { let expectation = self.expectation(description: "container") let description = NSPersistentStoreDescription() @@ -752,6 +778,24 @@ private extension NSManagedObjectContext { try save() } + func insertNotes( + _ notes: (text: String, creationDate: Date, numberOfViews: Int, tags: [String], attachment: Attachment?)... + ) 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 + + if let attachment = description.attachment { + note.attachment = attachment + } + } + + try save() + } + func insertAccounts(purchases: [[Double]]) throws { for description in purchases { let account = NSEntityDescription.insertNewObject(forEntityName: "Account", into: self) as! Account @@ -793,6 +837,15 @@ private extension NSManagedObjectContext { try save() } + func insertAttachment(_ id: String) throws -> Attachment { + let attachment = NSEntityDescription.insertNewObject(forEntityName: "Attachment", into: self) as! Attachment + attachment.id = id + + try save() + + return attachment + } + func deleteAll(_ type: T.Type) { let fetchRequest = NSFetchRequest(entityName: String(describing: T.self)) fetchRequest.includesPropertyValues = false diff --git a/PredicateKitTests/OperatorTests.swift b/PredicateKitTests/OperatorTests.swift index 7873889..6446f3b 100644 --- a/PredicateKitTests/OperatorTests.swift +++ b/PredicateKitTests/OperatorTests.swift @@ -236,6 +236,37 @@ final class OperatorTests: XCTestCase { XCTAssertEqual(value, 42) } + @available(iOS 13.0, *) + func testKeyPathEqualIdentifiable() throws { + struct Data { + let identifiable: IdentifiableData + } + + struct IdentifiableData: Identifiable, Equatable { + let id: String + } + + let predicate = \Data.identifiable == IdentifiableData(id: "1") + + guard case let .comparison(comparison) = predicate else { + XCTFail("identifiable.id == 1 should result in a comparison") + return + } + + guard + let expression = comparison.expression.as(ObjectIdentifier, String>.self) + else { + XCTFail("the left side of the comparison should be a key path expression") + return + } + + let value = try XCTUnwrap(comparison.value as? IdentifiableData.ID) + + XCTAssertEqual(expression.root, \Data.identifiable) + XCTAssertEqual(comparison.operator, .equal) + XCTAssertEqual(value, "1") + } + func testOptionalKeyPathEqualToNil() throws { let predicate: Predicate = \Data.optionalRelationship == nil @@ -2197,6 +2228,7 @@ private struct Data { let creationDate: Date let optionalRelationship: Relationship? let optionalRelationships: [Relationship]? + let identifiable: IdentifiableData? } private struct Relationship { diff --git a/PredicateKitTests/Resources/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents b/PredicateKitTests/Resources/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents index b21271a..c11eeb7 100644 --- a/PredicateKitTests/Resources/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents +++ b/PredicateKitTests/Resources/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents @@ -1,18 +1,25 @@ - + + + + + + + + @@ -26,12 +33,4 @@ - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 67c5bc4..0beca34 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,7 @@ class Note: NSManagedObject { @NSManaged var creationDate: Date @NSManaged var numberOfViews: Int @NSManaged var tags: [String] + @NSManaged var attachment: Attachment } // Matches all notes where the text is equal to "Hello, World!". @@ -287,6 +288,9 @@ 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`. +let predicate = \Note.attachment == attachment ``` #### String comparisons