From f99c83f0def6d82c3d4b2f74668fa396f7c2dc6b Mon Sep 17 00:00:00 2001 From: Omar Allaham Date: Tue, 15 Mar 2022 12:33:16 +0100 Subject: [PATCH 01/11] allow "IN" Expression to have a list or a set as a parameter this is because for values like Int64 passing an array to an IN expression is not allowed and requires variadic parameter --- PredicateKit/Predicate.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/PredicateKit/Predicate.swift b/PredicateKit/Predicate.swift index 803d5a4..05e0f60 100644 --- a/PredicateKit/Predicate.swift +++ b/PredicateKit/Predicate.swift @@ -477,6 +477,14 @@ extension Expression where Value: Primitive { public func `in`(_ list: Value...) -> Predicate { .comparison(.init(self, .in, list)) } + + public func `in`(_ list: [Value]) -> Predicate { + .comparison(.init(self, .in, list)) + } + + public func `in`(_ set: Set) -> Predicate where Value: Hashable { + .comparison(.init(self, .in, Array(set))) + } } extension Expression where Value: StringValue & Primitive { From 9ee880012b1ca482c4f10faf4e872abcc32b3540 Mon Sep 17 00:00:00 2001 From: Omar Allaham Date: Mon, 21 Mar 2022 14:38:46 +0100 Subject: [PATCH 02/11] added tests for 'IN' expression with array and set arguments --- PredicateKitTests/OperatorTests.swift | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/PredicateKitTests/OperatorTests.swift b/PredicateKitTests/OperatorTests.swift index c359bf7..3b9539c 100644 --- a/PredicateKitTests/OperatorTests.swift +++ b/PredicateKitTests/OperatorTests.swift @@ -1335,6 +1335,49 @@ final class OperatorTests: XCTestCase { XCTAssertEqual(comparison.operator, .in) XCTAssertEqual(value, ["hello", "world", "welcome"]) } + + func testKeyPathInArray() throws { + let predicate: Predicate = (\Data.text).in(["hello", "world", "welcome"]) + + guard case let .comparison(comparison) = predicate else { + XCTFail("text.in(['hello', 'world', 'welcome']) should result in a comparison") + return + } + + guard let keyPath = comparison.expression.as(KeyPath.self) else { + XCTFail("the left side of the comparison should be a key path expression") + return + } + + let value = try XCTUnwrap(comparison.value as? [String]) + + XCTAssertEqual(keyPath, \Data.text) + XCTAssertEqual(comparison.operator, .in) + XCTAssertEqual(value, ["hello", "world", "welcome"]) + } + + func testKeyPathInSet() throws { + let predicate: Predicate = (\Data.text).in(Set(["hello", "world", "welcome"])) + + guard case let .comparison(comparison) = predicate else { + XCTFail("text.in(Set(['hello', 'world', 'welcome'])) should result in a comparison") + return + } + + guard let keyPath = comparison.expression.as(KeyPath.self) else { + XCTFail("the left side of the comparison should be a key path expression") + return + } + + let value = try XCTUnwrap(comparison.value as? [String]) + + XCTAssertEqual(keyPath, \Data.text) + XCTAssertEqual(comparison.operator, .in) + XCTAssertTrue(value.contains("hello"), "searched item '\("hello")' is missing in comparison.value") + XCTAssertTrue(value.contains("world"), "searched item '\("world")' is missing in comparison.value") + XCTAssertTrue(value.contains("welcome"), "searched item '\("welcome")' is missing in comparison.value") + XCTAssertEqual(value.count, 3, "IN expression had \(3) items, found \(value.count)") + } func testKeyPathInCaseInsensitive() throws { let predicate: Predicate = (\Data.text).in(["hello", "world", "welcome"], .caseInsensitive) From d95fec26a7e6e4ac0e062ccb5a648d037c7dc109 Mon Sep 17 00:00:00 2001 From: Omar Allaham Date: Mon, 21 Mar 2022 14:44:00 +0100 Subject: [PATCH 03/11] update README file with examples for "IN" with an array or a set --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index b7ff1de..0fec851 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,19 @@ You can use the `in` function to determine whether a property's value is one o // Matches all notes where the text is one of the elements in the specified list. let predicate = (\Note.text).in("a", "b", "c", "d") ``` +or pass in an Array + +```swift +// Matches all notes where the text is one of the elements in the specified list. +let predicate = (\Note.text).in(["a", "b", "c", "d"]) +``` + +or pass in a Set + +```swift +// Matches all notes where the text is one of the elements in the specified list. +let predicate = (\Note.text).in(Set(["a", "b", "c", "d"])) +``` When the property is of type `String`, `in` accepts a second parameter that determines how the string should be compared to the elements in the list. From f4688d885fca6997ee4bce17be20eabd2caa7c6a Mon Sep 17 00:00:00 2001 From: Omar Allaham Date: Wed, 23 Mar 2022 09:52:05 +0100 Subject: [PATCH 04/11] update the README file for better description of 'IN' function --- README.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0fec851..22399d3 100644 --- a/README.md +++ b/README.md @@ -341,28 +341,21 @@ let predicate = \Note.numberOfViews ~= 100...200 ###### in -You can use the `in` function to determine whether a property's value is one of the values in a specified list. +You can use the `in` function to determine whether a property's value is one of the values in a variadic arguments (comma-separated list), an array, or a set. ```swift -// Matches all notes where the text is one of the elements in the specified list. +// Matches all notes where the text is one of the elements in the specified variadic arguments list. let predicate = (\Note.text).in("a", "b", "c", "d") -``` -or pass in an Array -```swift -// Matches all notes where the text is one of the elements in the specified list. +// Matches all notes where the text is one of the elements in the specified array. let predicate = (\Note.text).in(["a", "b", "c", "d"]) -``` -or pass in a Set - -```swift -// Matches all notes where the text is one of the elements in the specified list. +// Matches all notes where the text is one of the elements in the specified set. let predicate = (\Note.text).in(Set(["a", "b", "c", "d"])) ``` -When the property is of type `String`, `in` accepts a second parameter that determines how the string should be compared to the elements in the list. +When the array `Element` is a `String`, `in` accepts a second parameter that determines how the string should be compared to the elements in the list. ```swift // Case-insensitive comparison. From b7f60dc653371a2ea57aac387a03a022a10e57c7 Mon Sep 17 00:00:00 2001 From: Omar Allaham Date: Wed, 23 Mar 2022 09:59:54 +0100 Subject: [PATCH 05/11] fix 'in' tests to use to correct function for the array and set --- PredicateKitTests/OperatorTests.swift | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/PredicateKitTests/OperatorTests.swift b/PredicateKitTests/OperatorTests.swift index 3b9539c..7873889 100644 --- a/PredicateKitTests/OperatorTests.swift +++ b/PredicateKitTests/OperatorTests.swift @@ -1337,45 +1337,45 @@ final class OperatorTests: XCTestCase { } func testKeyPathInArray() throws { - let predicate: Predicate = (\Data.text).in(["hello", "world", "welcome"]) + let predicate: Predicate = (\Data.count).in([21, 42, 63]) guard case let .comparison(comparison) = predicate else { - XCTFail("text.in(['hello', 'world', 'welcome']) should result in a comparison") + XCTFail("count.in([21, 42, 63]) should result in a comparison") return } - guard let keyPath = comparison.expression.as(KeyPath.self) else { + guard let keyPath = comparison.expression.as(KeyPath.self) else { XCTFail("the left side of the comparison should be a key path expression") return } - let value = try XCTUnwrap(comparison.value as? [String]) + let value = try XCTUnwrap(comparison.value as? [Int]) - XCTAssertEqual(keyPath, \Data.text) + XCTAssertEqual(keyPath, \Data.count) XCTAssertEqual(comparison.operator, .in) - XCTAssertEqual(value, ["hello", "world", "welcome"]) + XCTAssertEqual(value, [21, 42, 63]) } func testKeyPathInSet() throws { - let predicate: Predicate = (\Data.text).in(Set(["hello", "world", "welcome"])) + let predicate: Predicate = (\Data.count).in(Set([21, 42, 63])) guard case let .comparison(comparison) = predicate else { - XCTFail("text.in(Set(['hello', 'world', 'welcome'])) should result in a comparison") + XCTFail("count.in(Set([21, 42, 63])) should result in a comparison") return } - guard let keyPath = comparison.expression.as(KeyPath.self) else { + guard let keyPath = comparison.expression.as(KeyPath.self) else { XCTFail("the left side of the comparison should be a key path expression") return } - let value = try XCTUnwrap(comparison.value as? [String]) + let value = try XCTUnwrap(comparison.value as? [Int]) - XCTAssertEqual(keyPath, \Data.text) + XCTAssertEqual(keyPath, \Data.count) XCTAssertEqual(comparison.operator, .in) - XCTAssertTrue(value.contains("hello"), "searched item '\("hello")' is missing in comparison.value") - XCTAssertTrue(value.contains("world"), "searched item '\("world")' is missing in comparison.value") - XCTAssertTrue(value.contains("welcome"), "searched item '\("welcome")' is missing in comparison.value") + XCTAssertTrue(value.contains(21), "searched item '\(21)' is missing in comparison.value") + XCTAssertTrue(value.contains(42), "searched item '\(42)' is missing in comparison.value") + XCTAssertTrue(value.contains(63), "searched item '\(63)' is missing in comparison.value") XCTAssertEqual(value.count, 3, "IN expression had \(3) items, found \(value.count)") } From 9efd3669c856e61ea6d0d1c614bb0d64a9a8430d Mon Sep 17 00:00:00 2001 From: Omar Allaham Date: Wed, 23 Mar 2022 10:06:06 +0100 Subject: [PATCH 06/11] Fix ambiguous description of the in function with second parameter --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22399d3..d5dc389 100644 --- a/README.md +++ b/README.md @@ -355,7 +355,7 @@ let predicate = (\Note.text).in(["a", "b", "c", "d"]) let predicate = (\Note.text).in(Set(["a", "b", "c", "d"])) ``` -When the array `Element` is a `String`, `in` accepts a second parameter that determines how the string should be compared to the elements in the list. +When the property type is a `String`, `in` accepts a second parameter that determines how the string should be compared to the elements in the list. ```swift // Case-insensitive comparison. From 71faa3859e9970146f461576e1b1d04a363be9ab Mon Sep 17 00:00:00 2001 From: Omar Allaham Date: Mon, 23 May 2022 10:57:42 +0200 Subject: [PATCH 07/11] Adding explicit result function This is to allow chaining on the result as an example if a limit is set to one this would allow for chaining on the result's first item as follows: ``` let notes = try managedObjectContext .fetch(where: (\Note.text).contains("Hello, World!")) .sorted(by: \.creationDate, .descending) .entityResults() .first ``` --- .../NSManagedObjectContextExtensions.swift | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift b/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift index a6edbd8..99d03c4 100644 --- a/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift +++ b/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift @@ -334,7 +334,24 @@ public struct FetchRequest { #endif return try context.fetch(request) } - + + // MARK: - + + /// Executes the fetch request. + /// + /// - Returns: An array of objects of type `Entity` matching the criteria specified by the fetch request. + /// + /// ## Example + /// + /// let notes: [Note] = try managedObjectContext + /// .fetch(where: (\Note.text).contains("Hello, World!")) + /// .sorted(by: \.creationDate, .descending) + /// .result() + /// + public func entityResults() throws -> [Entity] { + try result() + } + /// Executes the fetch request. /// /// - Returns: An array of `[String: Any]` containing the keys or a subset of the keys of the objects of type `Entity` @@ -356,6 +373,22 @@ public struct FetchRequest { return try context.fetch(request) as! [[String: Any]] } + /// Executes the fetch request. + /// + /// - Returns: An array of `[String: Any]` containing the keys or a subset of the keys of the objects of type `Entity` + /// matching the criteria specified by the fetch request. + /// + /// ## Example + /// + /// let dictionaries: [[String: Any]] = try managedObjectContext + /// .fetch(where: (\Note.text).contains("Hello, World!")) + /// .sorted(by: \.creationDate, .descending) + /// .fetchingOnly(\.text, \.creationDate) + /// + func dictionaryResults() throws -> [[String: Any]] { + try result() + } + /// Counts the number of objects matching the criteria specified by the fetch request. /// /// - Returns: The number of objects matching the criteria specified by the fetch request. From a75c918db367ac1a3e5241efd21aec449797c9ec Mon Sep 17 00:00:00 2001 From: Omar Allaham Date: Mon, 23 May 2022 11:03:17 +0200 Subject: [PATCH 08/11] Added tests to ensure the explicit results return similar results to the substituted functions --- ...SManagedObjectContextExtensionsTests.swift | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift b/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift index b7fb98c..e73b6ef 100644 --- a/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift +++ b/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift @@ -115,6 +115,26 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase { XCTAssertNil(texts.first?["numberOfViews"]) XCTAssertNil(texts.first?["creationDate"]) } + + func testFetchDictionaryResultsWithBasicComparison() throws { + try container.viewContext.insertNotes( + (text: "Hello, World!", creationDate: Date(), numberOfViews: 42, tags: ["greeting"]), + (text: "Goodbye!", creationDate: Date(), numberOfViews: 3, tags: ["greeting"]) + ) + + let texts: [[String: Any]] = try container + .viewContext + .fetch(where: \Note.text == "Hello, World!") + .fetchingOnly(\Note.text) + .dictionaryResults() + + XCTAssertEqual(texts.count, 1) + XCTAssertEqual(texts.first?.count, 1) + XCTAssertEqual(texts.first?["text"] as? String, "Hello, World!") + XCTAssertNil(texts.first?["tags"]) + XCTAssertNil(texts.first?["numberOfViews"]) + XCTAssertNil(texts.first?["creationDate"]) + } func testFetchAll() throws { try container.viewContext.insertNotes( @@ -130,6 +150,21 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase { XCTAssertTrue(notes.contains(where: { $0.text == "Hello, World!" })) XCTAssertTrue(notes.contains(where: { $0.text == "Goodbye!" })) } + + func testFetchAllEntityResults() throws { + try container.viewContext.insertNotes( + (text: "Hello, World!", creationDate: Date(), numberOfViews: 42, tags: ["greeting"]), + (text: "Goodbye!", creationDate: Date(), numberOfViews: 3, tags: ["greeting"]) + ) + + let notes: [Note] = try container.viewContext + .fetchAll() + .entityResults() + + XCTAssertEqual(notes.count, 2) + XCTAssertTrue(notes.contains(where: { $0.text == "Hello, World!" })) + XCTAssertTrue(notes.contains(where: { $0.text == "Goodbye!" })) + } func testFetchWithStringComparison1() throws { try container.viewContext.insertNotes( From 9ec5b4b41c2697661d9de09582bdc6345d816caa Mon Sep 17 00:00:00 2001 From: Omar Allaham Date: Mon, 23 May 2022 11:14:30 +0200 Subject: [PATCH 09/11] fix function names to match the previous naming convention --- .../CoreData/NSManagedObjectContextExtensions.swift | 7 ++++--- .../NSManagedObjectContextExtensionsTests.swift | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift b/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift index 99d03c4..dccc707 100644 --- a/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift +++ b/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift @@ -346,9 +346,9 @@ public struct FetchRequest { /// let notes: [Note] = try managedObjectContext /// .fetch(where: (\Note.text).contains("Hello, World!")) /// .sorted(by: \.creationDate, .descending) - /// .result() + /// .entityResult() /// - public func entityResults() throws -> [Entity] { + public func entityResult() throws -> [Entity] { try result() } @@ -384,8 +384,9 @@ public struct FetchRequest { /// .fetch(where: (\Note.text).contains("Hello, World!")) /// .sorted(by: \.creationDate, .descending) /// .fetchingOnly(\.text, \.creationDate) + /// .dictionaryResult() /// - func dictionaryResults() throws -> [[String: Any]] { + func dictionaryResult() throws -> [[String: Any]] { try result() } diff --git a/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift b/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift index e73b6ef..7724e04 100644 --- a/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift +++ b/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift @@ -116,7 +116,7 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase { XCTAssertNil(texts.first?["creationDate"]) } - func testFetchDictionaryResultsWithBasicComparison() throws { + func testFetchDictionaryResultWithBasicComparison() throws { try container.viewContext.insertNotes( (text: "Hello, World!", creationDate: Date(), numberOfViews: 42, tags: ["greeting"]), (text: "Goodbye!", creationDate: Date(), numberOfViews: 3, tags: ["greeting"]) @@ -126,7 +126,7 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase { .viewContext .fetch(where: \Note.text == "Hello, World!") .fetchingOnly(\Note.text) - .dictionaryResults() + .dictionaryResult() XCTAssertEqual(texts.count, 1) XCTAssertEqual(texts.first?.count, 1) @@ -159,7 +159,7 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase { let notes: [Note] = try container.viewContext .fetchAll() - .entityResults() + .entityResult() XCTAssertEqual(notes.count, 2) XCTAssertTrue(notes.contains(where: { $0.text == "Hello, World!" })) From 0fa95f0d8e4f91a3864bf8ecca50a880fa92ba04 Mon Sep 17 00:00:00 2001 From: Omar Allaham Date: Mon, 23 May 2022 11:27:14 +0200 Subject: [PATCH 10/11] added description to ReadME about the result function usage --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5dc389..c6ba4ed 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ dependencies: [ ## Fetching objects -To fetch objects using PredicateKit, use the function `fetch(where:)` on an instance of `NSManagedObjectContext` passing as argument a predicate. `fetch(where:)` returns an object of type `FetchRequest` on which you call `result()` to execute the request and retrieve the matching objects. +To fetch objects using PredicateKit, use the function `fetch(where:)` on an instance of `NSManagedObjectContext` passing as argument a predicate. `fetch(where:)` returns an object of type `FetchRequest` on which you call `result()` (or `entityResult()` to allow function chaining) to execute the request and retrieve the matching objects. ###### Example @@ -118,6 +118,11 @@ To fetch objects using PredicateKit, use the function `fetch(where:)` on an inst let notes: [Note] = try managedObjectContext .fetch(where: \Note.text == "Hello, World!" && \Note.creationDate < Date()) .result() + +let notes = try managedObjectContext + .fetch(where: \Note.text == "Hello, World!" && \Note.creationDate < Date()) + .entityResult() + .first ``` You write your predicates using the [key-paths](https://developer.apple.com/documentation/swift/keypath) of the entity to filter and a combination of comparison and logical operators, literal values, and functions calls. @@ -126,8 +131,8 @@ See [Writing predicates](#writing-predicates) for more about writing predicates. ### Fetching objects as dictionaries -By default, `fetch(where:)` returns an array of subclasses of `NSManagedObject`. You can specify that the objects be returned as an array of dictionaries (`[[String: Any]]`) -simply by changing the type of the variable storing the result of the fetch. +By default, `fetch(where:)` returns an array of subclasses of `NSManagedObject`. You can specify that the objects be returned as an array of dictionaries `[[String: Any]]` +simply by changing the type of the variable storing the result of the fetch or specifically calling `dictionaryResult()`. ###### Example @@ -135,6 +140,10 @@ simply by changing the type of the variable storing the result of the fetch. let notes: [[String: Any]] = try managedObjectContext .fetch(where: \Note.text == "Hello, World!" && \Note.creationDate < Date()) .result() + +let notes = try managedObjectContext + .fetch(where: \Note.text == "Hello, World!" && \Note.creationDate < Date()) + .dictionaryResult() ``` ## Configuring the fetch From a1beab55cb5f757deb8dcce0b6725b156398563a Mon Sep 17 00:00:00 2001 From: Omar Allaham Date: Tue, 31 May 2022 15:34:19 +0200 Subject: [PATCH 11/11] added missing public accessor for dictionaryResult --- PredicateKit/CoreData/NSManagedObjectContextExtensions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift b/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift index dccc707..0f0efb1 100644 --- a/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift +++ b/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift @@ -386,7 +386,7 @@ public struct FetchRequest { /// .fetchingOnly(\.text, \.creationDate) /// .dictionaryResult() /// - func dictionaryResult() throws -> [[String: Any]] { + public func dictionaryResult() throws -> [[String: Any]] { try result() }