From c1c63623f4eaca3dd4478cc39bc0e6cc98f30e25 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Wed, 11 Dec 2024 16:41:10 -0800 Subject: [PATCH] Rename the failable map overload to compactMap And also provide another overload of map that takes in an optional and behaves much more closely to what the original map matcher does --- Sources/Nimble/Matchers/Map.swift | 34 ++++++++- Tests/NimbleTests/Matchers/MapTest.swift | 87 ++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/Sources/Nimble/Matchers/Map.swift b/Sources/Nimble/Matchers/Map.swift index 509a49799..5029f98be 100644 --- a/Sources/Nimble/Matchers/Map.swift +++ b/Sources/Nimble/Matchers/Map.swift @@ -29,9 +29,37 @@ public func map(_ transform: @escaping (T) async throws -> U, _ matcher: s /// `map` works by transforming the expression to a value that the given matcher uses. /// /// For example, you might only care that a particular property on a method equals some other value. +/// So, you could write `expect(myObject).to(map(\.someOptionalIntValue, equal(3))`. +/// This is also useful in conjunction with ``satisfyAllOf`` to do a partial equality of an object. +public func map(_ transform: @escaping (T) throws -> U?, _ matcher: Matcher) -> Matcher { + Matcher { (received: Expression) in + try matcher.satisfies(received.cast { value in + guard let value else { return nil } + return try transform(value) + }) + } +} + +/// `map` works by transforming the expression to a value that the given matcher uses. +/// +/// For example, you might only care that a particular property on a method equals some other value. +/// So, you could write `expect(myObject).to(map(\.someOptionalIntValue, equal(3))`. +/// This is also useful in conjunction with ``satisfyAllOf`` to do a partial equality of an object. +public func map(_ transform: @escaping (T) async throws -> U?, _ matcher: some AsyncableMatcher) -> AsyncMatcher { + AsyncMatcher { (received: AsyncExpression) in + try await matcher.satisfies(received.cast { value in + guard let value else { return nil } + return try await transform(value) + }) + } +} + +/// `compactMap` works by transforming the expression to a value that the given matcher uses. +/// +/// For example, you might only care that a particular property on a method equals some other value. /// So, you could write `expect(myObject).to(compactMap({ $0 as? Int }, equal(3))`. /// This is also useful in conjunction with ``satisfyAllOf`` to match against a converted type. -public func map(_ transform: @escaping (T) throws -> U?, _ matcher: Matcher) -> Matcher { +public func compactMap(_ transform: @escaping (T) throws -> U?, _ matcher: Matcher) -> Matcher { Matcher { (received: Expression) in let message = ExpectationMessage.expectedTo("Map from \(T.self) to \(U.self)") @@ -47,12 +75,12 @@ public func map(_ transform: @escaping (T) throws -> U?, _ matcher: Matche } } -/// `map` works by transforming the expression to a value that the given matcher uses. +/// `compactMap` works by transforming the expression to a value that the given matcher uses. /// /// For example, you might only care that a particular property on a method equals some other value. /// So, you could write `expect(myObject).to(compactMap({ $0 as? Int }, equal(3))`. /// This is also useful in conjunction with ``satisfyAllOf`` to match against a converted type. -public func map(_ transform: @escaping (T) async throws -> U?, _ matcher: some AsyncableMatcher) -> AsyncMatcher { +public func compactMap(_ transform: @escaping (T) async throws -> U?, _ matcher: some AsyncableMatcher) -> AsyncMatcher { AsyncMatcher { (received: AsyncExpression) in let message = ExpectationMessage.expectedTo("Map from \(T.self) to \(U.self)") diff --git a/Tests/NimbleTests/Matchers/MapTest.swift b/Tests/NimbleTests/Matchers/MapTest.swift index cc361f4df..e7413deaa 100644 --- a/Tests/NimbleTests/Matchers/MapTest.swift +++ b/Tests/NimbleTests/Matchers/MapTest.swift @@ -158,4 +158,91 @@ final class MapTest: XCTestCase { map(\.string, equal("world")) )) } + + // MARK: Compact map + func testCompactMap() { + expect("1").to(compactMap({ Int($0) }, equal(1))) + expect("1").toNot(compactMap({ Int($0) }, equal(2))) + + let assertions = gatherExpectations(silently: true) { + expect("not a number").to(compactMap({ Int($0) }, equal(1))) + expect("not a number").toNot(compactMap({ Int($0) }, equal(1))) + } + + expect(assertions).to(haveCount(2)) + expect(assertions.first?.success).to(beFalse()) + expect(assertions.last?.success).to(beFalse()) + } + + func testCompactMapAsync() async { + struct Value { + let int: Int? + let string: String? + } + + await expect("1").to(compactMap({ Int($0) }, asyncEqual(1))) + await expect("1").toNot(compactMap({ Int($0) }, asyncEqual(2))) + + let assertions = await gatherExpectations(silently: true) { + await expect("not a number").to(compactMap({ Int($0) }, asyncEqual(1))) + await expect("not a number").toNot(compactMap({ Int($0) }, asyncEqual(1))) + } + + expect(assertions).to(haveCount(2)) + expect(assertions.first?.success).to(beFalse()) + expect(assertions.last?.success).to(beFalse()) + } + + func testCompactMapWithAsyncFunction() async { + func someOperation(_ value: Int) async -> String? { + "\(value)" + } + await expect(1).to(compactMap(someOperation, equal("1"))) + + func someFailingOperation(_ value: Int) async -> String? { + nil + } + + let assertions = await gatherExpectations(silently: true) { + await expect(1).to(compactMap(someFailingOperation, equal("1"))) + await expect(1).toNot(compactMap(someFailingOperation, equal("1"))) + } + + expect(assertions).to(haveCount(2)) + expect(assertions.first?.success).to(beFalse()) + expect(assertions.last?.success).to(beFalse()) + } + + func testCompactMapWithActor() { + actor Box { + let int: Int? + let string: String? + + init(int: Int?, string: String?) { + self.int = int + self.string = string + } + } + + let box = Box(int: 3, string: "world") + + expect(box).to(satisfyAllOf( + compactMap(\.int, equal(3)), + compactMap(\.string, equal("world")) + )) + + let failingBox = Box(int: nil, string: nil) + + let assertions = gatherExpectations(silently: true) { + expect(failingBox).to(satisfyAllOf( + compactMap(\.int, equal(3)) + )) + expect(failingBox).toNot(satisfyAllOf( + compactMap(\.int, equal(3)) + )) + } + expect(assertions).to(haveCount(2)) + expect(assertions.first?.success).to(beFalse()) + expect(assertions.last?.success).to(beFalse()) + } }