diff --git a/Sources/Nimble/Matchers/Map.swift b/Sources/Nimble/Matchers/Map.swift index 7132ab3c4..509a49799 100644 --- a/Sources/Nimble/Matchers/Map.swift +++ b/Sources/Nimble/Matchers/Map.swift @@ -1,7 +1,7 @@ /// `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(lens(\.someIntValue, equal(3))`. +/// So, you could write `expect(myObject).to(map(\.someIntValue, 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 @@ -15,7 +15,7 @@ public func map(_ transform: @escaping (T) throws -> U, _ matcher: Matcher /// `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(lens(\.someIntValue, equal(3))`. +/// So, you could write `expect(myObject).to(map(\.someIntValue, 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 @@ -25,3 +25,45 @@ 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(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 { + Matcher { (received: Expression) in + let message = ExpectationMessage.expectedTo("Map from \(T.self) to \(U.self)") + + guard let value = try received.evaluate() else { + return MatcherResult(status: .fail, message: message.appendedBeNilHint()) + } + + guard let transformedValue = try transform(value) else { + return MatcherResult(status: .fail, message: message) + } + + return try matcher.satisfies(Expression(expression: { transformedValue }, location: received.location)) + } +} + +/// `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(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 { + AsyncMatcher { (received: AsyncExpression) in + let message = ExpectationMessage.expectedTo("Map from \(T.self) to \(U.self)") + + guard let value = try await received.evaluate() else { + return MatcherResult(status: .fail, message: message.appendedBeNilHint()) + } + + guard let transformedValue = try await transform(value) else { + return MatcherResult(status: .fail, message: message) + } + + return try await matcher.satisfies(AsyncExpression(expression: { transformedValue }, location: received.location)) + } +} diff --git a/Tests/NimbleTests/Matchers/MapTest.swift b/Tests/NimbleTests/Matchers/MapTest.swift index 9b13c5f64..cc361f4df 100644 --- a/Tests/NimbleTests/Matchers/MapTest.swift +++ b/Tests/NimbleTests/Matchers/MapTest.swift @@ -5,6 +5,7 @@ import NimbleSharedTestHelpers #endif final class MapTest: XCTestCase { + // MARK: Map func testMap() { expect(1).to(map({ $0 }, equal(1))) @@ -80,4 +81,81 @@ final class MapTest: XCTestCase { map(\.string, equal("world")) )) } + + // MARK: Failable map + func testFailableMap() { + expect("1").to(map({ Int($0) }, equal(1))) + + struct Value { + let int: Int? + let string: String? + } + + expect(Value( + int: 1, + string: "hello" + )).to(satisfyAllOf( + map(\.int, equal(1)), + map(\.string, equal("hello")) + )) + + expect(Value( + int: 1, + string: "hello" + )).to(satisfyAnyOf( + map(\.int, equal(2)), + map(\.string, equal("hello")) + )) + + expect(Value( + int: 1, + string: "hello" + )).toNot(satisfyAllOf( + map(\.int, equal(2)), + map(\.string, equal("hello")) + )) + } + + func testFailableMapAsync() async { + struct Value { + let int: Int? + let string: String? + } + + await expect(Value( + int: 1, + string: "hello" + )).to(map(\.int, asyncEqual(1))) + + await expect(Value( + int: 1, + string: "hello" + )).toNot(map(\.int, asyncEqual(2))) + } + + func testFailableMapWithAsyncFunction() async { + func someOperation(_ value: Int) async -> String? { + "\(value)" + } + await expect(1).to(map(someOperation, equal("1"))) + } + + func testFailableMapWithActor() { + 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( + map(\.int, equal(3)), + map(\.string, equal("world")) + )) + } }