diff --git a/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift b/PredicateKit/CoreData/NSManagedObjectContextExtensions.swift index a6edbd8..0f0efb1 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) + /// .entityResult() + /// + public func entityResult() 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,23 @@ 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) + /// .dictionaryResult() + /// + public func dictionaryResult() 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. 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 { diff --git a/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift b/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift index b7fb98c..7724e04 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 testFetchDictionaryResultWithBasicComparison() 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) + .dictionaryResult() + + 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() + .entityResult() + + 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( diff --git a/PredicateKitTests/OperatorTests.swift b/PredicateKitTests/OperatorTests.swift index c359bf7..7873889 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.count).in([21, 42, 63]) + + guard case let .comparison(comparison) = predicate else { + XCTFail("count.in([21, 42, 63]) 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? [Int]) + + XCTAssertEqual(keyPath, \Data.count) + XCTAssertEqual(comparison.operator, .in) + XCTAssertEqual(value, [21, 42, 63]) + } + + func testKeyPathInSet() throws { + let predicate: Predicate = (\Data.count).in(Set([21, 42, 63])) + + guard case let .comparison(comparison) = predicate else { + XCTFail("count.in(Set([21, 42, 63])) 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? [Int]) + + XCTAssertEqual(keyPath, \Data.count) + XCTAssertEqual(comparison.operator, .in) + 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)") + } func testKeyPathInCaseInsensitive() throws { let predicate: Predicate = (\Data.text).in(["hello", "world", "welcome"], .caseInsensitive) diff --git a/README.md b/README.md index b7ff1de..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 @@ -341,15 +350,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") + +// Matches all notes where the text is one of the elements in the specified array. +let predicate = (\Note.text).in(["a", "b", "c", "d"]) + +// 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 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.