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()) + } }