Skip to content

Commit 74185db

Browse files
authored
Add an overload of map that allows you to fail the conversion by returning nil. (#1177)
1 parent 51d5438 commit 74185db

File tree

2 files changed

+122
-2
lines changed

2 files changed

+122
-2
lines changed

Sources/Nimble/Matchers/Map.swift

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/// `map` works by transforming the expression to a value that the given matcher uses.
22
///
33
/// For example, you might only care that a particular property on a method equals some other value.
4-
/// So, you could write `expect(myObject).to(lens(\.someIntValue, equal(3))`.
4+
/// So, you could write `expect(myObject).to(map(\.someIntValue, equal(3))`.
55
/// This is also useful in conjunction with ``satisfyAllOf`` to do a partial equality of an object.
66
public func map<T, U>(_ transform: @escaping (T) throws -> U, _ matcher: Matcher<U>) -> Matcher<T> {
77
Matcher { (received: Expression<T>) in
@@ -15,7 +15,7 @@ public func map<T, U>(_ transform: @escaping (T) throws -> U, _ matcher: Matcher
1515
/// `map` works by transforming the expression to a value that the given matcher uses.
1616
///
1717
/// For example, you might only care that a particular property on a method equals some other value.
18-
/// So, you could write `expect(myObject).to(lens(\.someIntValue, equal(3))`.
18+
/// So, you could write `expect(myObject).to(map(\.someIntValue, equal(3))`.
1919
/// This is also useful in conjunction with ``satisfyAllOf`` to do a partial equality of an object.
2020
public func map<T, U>(_ transform: @escaping (T) async throws -> U, _ matcher: some AsyncableMatcher<U>) -> AsyncMatcher<T> {
2121
AsyncMatcher { (received: AsyncExpression<T>) in
@@ -25,3 +25,45 @@ public func map<T, U>(_ transform: @escaping (T) async throws -> U, _ matcher: s
2525
})
2626
}
2727
}
28+
29+
/// `map` works by transforming the expression to a value that the given matcher uses.
30+
///
31+
/// For example, you might only care that a particular property on a method equals some other value.
32+
/// So, you could write `expect(myObject).to(compactMap({ $0 as? Int }, equal(3))`.
33+
/// This is also useful in conjunction with ``satisfyAllOf`` to match against a converted type.
34+
public func map<T, U>(_ transform: @escaping (T) throws -> U?, _ matcher: Matcher<U>) -> Matcher<T> {
35+
Matcher { (received: Expression<T>) in
36+
let message = ExpectationMessage.expectedTo("Map from \(T.self) to \(U.self)")
37+
38+
guard let value = try received.evaluate() else {
39+
return MatcherResult(status: .fail, message: message.appendedBeNilHint())
40+
}
41+
42+
guard let transformedValue = try transform(value) else {
43+
return MatcherResult(status: .fail, message: message)
44+
}
45+
46+
return try matcher.satisfies(Expression(expression: { transformedValue }, location: received.location))
47+
}
48+
}
49+
50+
/// `map` works by transforming the expression to a value that the given matcher uses.
51+
///
52+
/// For example, you might only care that a particular property on a method equals some other value.
53+
/// So, you could write `expect(myObject).to(compactMap({ $0 as? Int }, equal(3))`.
54+
/// This is also useful in conjunction with ``satisfyAllOf`` to match against a converted type.
55+
public func map<T, U>(_ transform: @escaping (T) async throws -> U?, _ matcher: some AsyncableMatcher<U>) -> AsyncMatcher<T> {
56+
AsyncMatcher { (received: AsyncExpression<T>) in
57+
let message = ExpectationMessage.expectedTo("Map from \(T.self) to \(U.self)")
58+
59+
guard let value = try await received.evaluate() else {
60+
return MatcherResult(status: .fail, message: message.appendedBeNilHint())
61+
}
62+
63+
guard let transformedValue = try await transform(value) else {
64+
return MatcherResult(status: .fail, message: message)
65+
}
66+
67+
return try await matcher.satisfies(AsyncExpression(expression: { transformedValue }, location: received.location))
68+
}
69+
}

Tests/NimbleTests/Matchers/MapTest.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import NimbleSharedTestHelpers
55
#endif
66

77
final class MapTest: XCTestCase {
8+
// MARK: Map
89
func testMap() {
910
expect(1).to(map({ $0 }, equal(1)))
1011

@@ -80,4 +81,81 @@ final class MapTest: XCTestCase {
8081
map(\.string, equal("world"))
8182
))
8283
}
84+
85+
// MARK: Failable map
86+
func testFailableMap() {
87+
expect("1").to(map({ Int($0) }, equal(1)))
88+
89+
struct Value {
90+
let int: Int?
91+
let string: String?
92+
}
93+
94+
expect(Value(
95+
int: 1,
96+
string: "hello"
97+
)).to(satisfyAllOf(
98+
map(\.int, equal(1)),
99+
map(\.string, equal("hello"))
100+
))
101+
102+
expect(Value(
103+
int: 1,
104+
string: "hello"
105+
)).to(satisfyAnyOf(
106+
map(\.int, equal(2)),
107+
map(\.string, equal("hello"))
108+
))
109+
110+
expect(Value(
111+
int: 1,
112+
string: "hello"
113+
)).toNot(satisfyAllOf(
114+
map(\.int, equal(2)),
115+
map(\.string, equal("hello"))
116+
))
117+
}
118+
119+
func testFailableMapAsync() async {
120+
struct Value {
121+
let int: Int?
122+
let string: String?
123+
}
124+
125+
await expect(Value(
126+
int: 1,
127+
string: "hello"
128+
)).to(map(\.int, asyncEqual(1)))
129+
130+
await expect(Value(
131+
int: 1,
132+
string: "hello"
133+
)).toNot(map(\.int, asyncEqual(2)))
134+
}
135+
136+
func testFailableMapWithAsyncFunction() async {
137+
func someOperation(_ value: Int) async -> String? {
138+
"\(value)"
139+
}
140+
await expect(1).to(map(someOperation, equal("1")))
141+
}
142+
143+
func testFailableMapWithActor() {
144+
actor Box {
145+
let int: Int?
146+
let string: String?
147+
148+
init(int: Int, string: String) {
149+
self.int = int
150+
self.string = string
151+
}
152+
}
153+
154+
let box = Box(int: 3, string: "world")
155+
156+
expect(box).to(satisfyAllOf(
157+
map(\.int, equal(3)),
158+
map(\.string, equal("world"))
159+
))
160+
}
83161
}

0 commit comments

Comments
 (0)