Skip to content

Commit

Permalink
Add an overload of map that allows you to fail the conversion by retu…
Browse files Browse the repository at this point in the history
…rning nil. (#1177)
  • Loading branch information
younata authored Dec 12, 2024
1 parent 51d5438 commit 74185db
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 2 deletions.
46 changes: 44 additions & 2 deletions Sources/Nimble/Matchers/Map.swift
Original file line number Diff line number Diff line change
@@ -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<T, U>(_ transform: @escaping (T) throws -> U, _ matcher: Matcher<U>) -> Matcher<T> {
Matcher { (received: Expression<T>) in
Expand All @@ -15,7 +15,7 @@ public func map<T, U>(_ 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<T, U>(_ transform: @escaping (T) async throws -> U, _ matcher: some AsyncableMatcher<U>) -> AsyncMatcher<T> {
AsyncMatcher { (received: AsyncExpression<T>) in
Expand All @@ -25,3 +25,45 @@ public func map<T, U>(_ 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<T, U>(_ transform: @escaping (T) throws -> U?, _ matcher: Matcher<U>) -> Matcher<T> {
Matcher { (received: Expression<T>) 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<T, U>(_ transform: @escaping (T) async throws -> U?, _ matcher: some AsyncableMatcher<U>) -> AsyncMatcher<T> {
AsyncMatcher { (received: AsyncExpression<T>) 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))
}
}
78 changes: 78 additions & 0 deletions Tests/NimbleTests/Matchers/MapTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import NimbleSharedTestHelpers
#endif

final class MapTest: XCTestCase {
// MARK: Map
func testMap() {
expect(1).to(map({ $0 }, equal(1)))

Expand Down Expand Up @@ -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"))
))
}
}

0 comments on commit 74185db

Please sign in to comment.