8000 Add support for testing objects for equality by ftchirou · Pull Request #20 · ftchirou/PredicateKit · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add support for testing objects for equality #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions PredicateKit/CoreData/NSFetchRequestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions PredicateKit/Predicate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,13 @@ public struct ArrayElementKeyPath<Array, Value>: Expression where Array: Express
let elementKeyPath: AnyKeyPath
}

public struct ObjectIdentifier<Object: Expression, Identifier: Primitive>: Expression {
public typealias Root = Object
public typealias Value = Identifier

let root: Object
}

enum ComparisonOperator {
case lessThan
case lessThanOrEqual
Expand Down Expand Up @@ -371,6 +378,11 @@ public func == <E: Expression, T: Equatable & Primitive> (lhs: E, rhs: T) -> Pre
.comparison(.init(lhs, .equal, rhs))
}

@available(iOS 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))
}

@_disfavoredOverload
public func == <E: Expression> (lhs: E, rhs: Nil) -> Predicate<E.Root> where E.Value: OptionalType {
.comparison(.init(lhs, .equal, rhs))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Data> = 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()
Expand Down Expand Up @@ -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: -
Expand All @@ -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 {
Expand All @@ -1111,6 +1152,10 @@ private class DataStore: NSAtomicStore {
}
}

class IdentifiableData: NSManagedObject, Identifiable {
@NSManaged var id: String
}

private func makeRequest<T: NSManagedObject>(_ predicate: Predicate<T>) -> FetchRequest<T> {
.init(context: NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType), predicate: predicate)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down Expand Up @@ -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 {
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<T: NSManagedObject>(_ type: T.Type) {
let fetchRequest = NSFetchRequest<T>(entityName: String(describing: T.self))
fetchRequest.includesPropertyValues = false
Expand Down
32 changes: 32 additions & 0 deletions PredicateKitTests/OperatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<KeyPath<Data, IdentifiableData>, 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> = \Data.optionalRelationship == nil

Expand Down Expand Up @@ -2197,6 +2228,7 @@ private struct Data {
let creationDate: Date
let optionalRelationship: Relationship?
let optionalRelationships: [Relationship]?
let identifiable: IdentifiableData?
}

private struct Relationship {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20C69" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22C65" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName=".Account" syncable="YES">
<attribute name="purchases" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName"/>
</entity>
<entity name="Attachment" representedClassName=".Attachment" syncable="YES">
<attribute name="id" optional="YES" attributeType="String"/>
</entity>
<entity name="BillingInfo" representedClassName=".BillingInfo" syncable="YES">
<attribute name="accountType" optional="YES" attributeType="String"/>
<attribute name="purchases" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName"/>
</entity>
<entity name="IdentifiableData" representedClassName=".IdentifiableData" syncable="YES">
<attribute name="id" optional="YES" attributeType="String"/>
</entity>
<entity name="Note" representedClassName=".Note" syncable="YES">
<attribute name="creationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="numberOfViews" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tags" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName"/>
<attribute name="text" optional="YES" attributeType="String"/>
<attribute name="updateDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="attachment" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Attachment"/>
</entity>
<entity name="Profile" representedClassName=".Profile" syncable="YES">
<attribute name="creationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
Expand All @@ -26,12 +33,4 @@
<attribute name="name" optional="YES" attributeType="String"/>
<relationship name="profiles" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Profile"/>
</entity>
<elements>
<element name="Account" positionX="0" positionY="0" width="128" height="44"/>
<element name="BillingInfo" positionX="0" positionY="0" width="128" height="59"/>
<element name="Note" positionX="0" positionY="0" width="128" height="104"/>
<element name="Profile" positionX="0" positionY="0" width="128" height="59"/>
<element name="User" positionX="0" positionY="0" width="128" height="59"/>
<element name="UserAccount" positionX="0" positionY="0" width="128" height="59"/>
</elements>
</model>
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!".
Expand All @@ -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
Expand Down
0