Skip to content

Commit

Permalink
✨ Allow enums, identifiables, and nils for notEqual (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
ftchirou authored Apr 4, 2024
1 parent 834065e commit 3b220e8
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 2 deletions.
15 changes: 14 additions & 1 deletion PredicateKit/Predicate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,6 @@ public func <= <E: Expression, T: RawRepresentable> (lhs: E, rhs: T) -> Predicat
.comparison(.init(lhs, .lessThanOrEqual, rhs.rawValue))
}


public func == <E: Expression, T: Equatable & Primitive> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T {
.comparison(.init(lhs, .equal, rhs))
}
Expand All @@ -406,6 +405,20 @@ public func != <E: Expression, T: Equatable & Primitive> (lhs: E, rhs: T) -> Pre
.comparison(.init(lhs, .notEqual, rhs))
}

public func != <E: Expression, T: RawRepresentable> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T, T.RawValue: Equatable & Primitive {
.comparison(.init(lhs, .notEqual, rhs.rawValue))
}

@available(iOS 13.0, watchOS 6.0, tvOS 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), .notEqual, rhs.id))
}

@_disfavoredOverload
public func != <E: Expression> (lhs: E, rhs: Nil) -> Predicate<E.Root> where E.Value: OptionalType {
.comparison(.init(lhs, .notEqual, rhs))
}

public func >= <E: Expression, T: Comparable & Primitive> (lhs: E, rhs: T) -> Predicate<E.Root> where E.Value == T {
.comparison(.init(lhs, .greaterThanOrEqual, rhs))
}
Expand Down
55 changes: 55 additions & 0 deletions PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,61 @@ final class NSFetchRequestBuilderTests: XCTestCase {
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

@available(iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func testNotEqualWithIdentifiable() 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, .notEqualTo)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

@available(iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func testNotEqualWithOptionalIdentifiable() throws {
guard let identifiable = makeIdentifiable() else {
XCTFail("could not initialize IdentifiableData")
return
}

identifiable.id = "42"

let request = makeRequest(\Data.optionalIdentifiable != 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: "optionalIdentifiable.id"))
XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: "42"))
XCTAssertEqual(comparison.predicateOperatorType, .notEqualTo)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testNotEqualWithRawRepresentable() throws {
let request = makeRequest(\Data.dataType != .two)
let builder = makeRequestBuilder()

let result: NSFetchRequest<Data> = 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, .notEqualTo)
XCTAssertEqual(comparison.comparisonPredicateModifier, .direct)
}

func testArrayElementNotEqualPredicate() throws {
let request = makeRequest((\Data.relationships).last(\.count) != 42)
let builder = makeRequestBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,27 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase {
XCTAssertEqual(notes.first?.numberOfViews, 42)
}

@available(iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func testFetchWithObjectComparison2() 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 != attachment2)
.result()

XCTAssertEqual(notes.count, 1)
XCTAssertEqual(notes.first?.text, "Hello, World!")
XCTAssertEqual(notes.first?.tags, ["greeting"])
XCTAssertEqual(notes.first?.numberOfViews, 42)
}

func testFetchWithEnumComparison() throws {
try container.viewContext.insertNotes(
(text: "Hello, World!", creationDate: Date(), numberOfViews: 42, tags: ["greeting"], type: .freeForm),
Expand All @@ -153,6 +174,22 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase {
XCTAssertEqual(notes.first?.numberOfViews, 42)
}

func testFetchWithEnumComparison2() 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 != .structured)
.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 @@ -684,7 +721,25 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase {
XCTAssertEqual(notes.first?.numberOfViews, 3)
}

func testFetchWithArrayNilEqualityNilEquality() throws {
func testFetchWithNilInequality() throws {
let now = Date()

try container.viewContext.insertNotes(
(text: "Hello, World!", creationDate: .distantFuture, updateDate: now, numberOfViews: 42, tags: ["greeting"]),
(text: "Goodbye!", creationDate: .distantPast, updateDate: nil, numberOfViews: 3, tags: ["greeting"])
)

let notes: [Note] = try container.viewContext
.fetch(where: \Note.updateDate != nil)
.result()

XCTAssertEqual(notes.count, 1)
XCTAssertEqual(notes.first?.text, "Hello, World!")
XCTAssertEqual(notes.first?.tags, ["greeting"])
XCTAssertEqual(notes.first?.numberOfViews, 42)
}

func testFetchWithArrayNilEquality() throws {
try container.viewContext.insertUsers(
(name: "John Doe", billingAccountType: "Pro", purchases: [35.0, 120.0]),
(name: "Jane Doe", billingAccountType: "Default", purchases: nil)
Expand Down
129 changes: 129 additions & 0 deletions PredicateKitTests/OperatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,37 @@ final class OperatorTests: XCTestCase {
XCTAssertEqual(value, "1")
}

func testKeyPathEqualRawRepresentable() throws {
struct Data {
let rawRepresentable: RawRepresentableValue
}

enum RawRepresentableValue: Int {
case zero
case one
}

let predicate = \Data.rawRepresentable == .zero

guard case let .comparison(comparison) = predicate else {
XCTFail("rawRepresentable == .zero should result in a comparison")
return
}

guard
let expression = comparison.expression.as(KeyPath<Data, RawRepresentableValue>.self)
else {
XCTFail("the left side of the comparison should be a key path")
return
}

let value = try XCTUnwrap(comparison.value as? RawRepresentableValue.RawValue)

XCTAssertEqual(expression, \Data.rawRepresentable)
XCTAssertEqual(comparison.operator, .equal)
XCTAssertEqual(value, 0)
}

func testOptionalKeyPathEqualToNil() throws {
let predicate: Predicate<Data> = \Data.optionalRelationship == nil

Expand Down Expand Up @@ -472,6 +503,104 @@ final class OperatorTests: XCTestCase {
XCTAssertEqual(value, 5)
}

@available(iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func testKeyPathNotEqualIdentifiable() 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, .notEqual)
XCTAssertEqual(value, "1")
}

func testKeyPathNotEqualRawRepresentable() throws {
struct Data {
let rawRepresentable: RawRepresentableValue
}

enum RawRepresentableValue: Int {
case zero
case one
}

let predicate = \Data.rawRepresentable != .zero

guard case let .comparison(comparison) = predicate else {
XCTFail("rawRepresentable != .zero should result in a comparison")
return
}

guard
let expression = comparison.expression.as(KeyPath<Data, RawRepresentableValue>.self)
else {
XCTFail("the left side of the comparison should be a key path")
return
}

let value = try XCTUnwrap(comparison.value as? RawRepresentableValue.RawValue)

XCTAssertEqual(expression, \Data.rawRepresentable)
XCTAssertEqual(comparison.operator, .notEqual)
XCTAssertEqual(value, 0)
}

func testOptionalKeyPathNotEqualToNil() throws {
let predicate: Predicate<Data> = \Data.optionalRelationship != nil

guard case let .comparison(comparison) = predicate else {
XCTFail("optionalRelationship != nil should result in a comparison")
return
}

guard let keyPath = comparison.expression.as(KeyPath<Data, Relationship?>.self) else {
XCTFail("the left side of the comparison should be a key path expression")
return
}

XCTAssertEqual(keyPath, \Data.optionalRelationship)
XCTAssertEqual(comparison.operator, .notEqual)
XCTAssertNotNil(comparison.value as? Nil)
}

func testOptionalArrayKeyPathNotEqualToNil() throws {
let predicate: Predicate<Data> = \Data.optionalRelationships != nil

guard case let .comparison(comparison) = predicate else {
XCTFail("optionalRelationships != nil should result in a comparison")
return
}

guard let keyPath = comparison.expression.as(KeyPath<Data, [Relationship]?>.self) else {
XCTFail("the left side of the comparison should be a key path expression")
return
}

XCTAssertEqual(keyPath, \Data.optionalRelationships)
XCTAssertEqual(comparison.operator, .notEqual)
XCTAssertNotNil(comparison.value as? Nil)
}

// MARK: - >=

func testKeyPathGreaterThanOrEqualPrimitive() throws {
Expand Down

0 comments on commit 3b220e8

Please sign in to comment.