Skip to content

Commit c95fa3d

Browse files
committed
Add throwing versions of attempt(_:) and attemptMap(_:)
1 parent 4e0785d commit c95fa3d

File tree

6 files changed

+306
-2
lines changed

6 files changed

+306
-2
lines changed

ReactiveSwift.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
4A0E11041D2A95200065D310 /* LifetimeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0E11031D2A95200065D310 /* LifetimeSpec.swift */; };
2020
4A0E11051D2A95200065D310 /* LifetimeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0E11031D2A95200065D310 /* LifetimeSpec.swift */; };
2121
4A0E11061D2A95200065D310 /* LifetimeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0E11031D2A95200065D310 /* LifetimeSpec.swift */; };
22+
4AC73ECB1DF273570004EC4F /* ResultExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */; };
23+
4AC73ECC1DF273570004EC4F /* ResultExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */; };
24+
4AC73ECD1DF273570004EC4F /* ResultExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */; };
25+
4AC73ECE1DF273570004EC4F /* ResultExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */; };
2226
579504331BB8A34200A5E482 /* BagSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C312EF19EF2A7700984962 /* BagSpec.swift */; };
2327
579504341BB8A34300A5E482 /* BagSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C312EF19EF2A7700984962 /* BagSpec.swift */; };
2428
57A4D1B11BA13D7A00F7D4B1 /* Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = D871D69E1B3B29A40070F16C /* Optional.swift */; };
@@ -225,6 +229,7 @@
225229
4A0AB6711DC28EFF00AA1E81 /* ReactiveExtensionsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveExtensionsSpec.swift; sourceTree = "<group>"; };
226230
4A0E10FE1D2A92720065D310 /* Lifetime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lifetime.swift; sourceTree = "<group>"; };
227231
4A0E11031D2A95200065D310 /* LifetimeSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifetimeSpec.swift; sourceTree = "<group>"; };
232+
4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultExtensions.swift; sourceTree = "<group>"; };
228233
57A4D2411BA13D7A00F7D4B1 /* ReactiveSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReactiveSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; };
229234
57A4D2441BA13F9700F7D4B1 /* tvOS-Application.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "tvOS-Application.xcconfig"; sourceTree = "<group>"; };
230235
57A4D2451BA13F9700F7D4B1 /* tvOS-Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "tvOS-Base.xcconfig"; sourceTree = "<group>"; };
@@ -405,6 +410,7 @@
405410
D03B4A3A19F4C26D009E02AC /* Internal Utilities */ = {
406411
isa = PBXGroup;
407412
children = (
413+
4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */,
408414
D00004081A46864E000E7D41 /* TupleExtensions.swift */,
409415
);
410416
name = "Internal Utilities";
@@ -869,6 +875,7 @@
869875
57A4D1B41BA13D7A00F7D4B1 /* Disposable.swift in Sources */,
870876
57A4D1B61BA13D7A00F7D4B1 /* Event.swift in Sources */,
871877
57A4D1B81BA13D7A00F7D4B1 /* Scheduler.swift in Sources */,
878+
4AC73ECE1DF273570004EC4F /* ResultExtensions.swift in Sources */,
872879
57A4D1B91BA13D7A00F7D4B1 /* Action.swift in Sources */,
873880
57A4D1BA1BA13D7A00F7D4B1 /* Property.swift in Sources */,
874881
9A090C171DA0309E00EE97CA /* Reactive.swift in Sources */,
@@ -920,6 +927,7 @@
920927
A9B315BC1B3940810001CB9C /* Disposable.swift in Sources */,
921928
A9B315BE1B3940810001CB9C /* Event.swift in Sources */,
922929
A9B315C01B3940810001CB9C /* Scheduler.swift in Sources */,
930+
4AC73ECD1DF273570004EC4F /* ResultExtensions.swift in Sources */,
923931
A9B315C11B3940810001CB9C /* Action.swift in Sources */,
924932
A9B315C21B3940810001CB9C /* Property.swift in Sources */,
925933
9A090C161DA0309E00EE97CA /* Reactive.swift in Sources */,
@@ -946,6 +954,7 @@
946954
D871D69F1B3B29A40070F16C /* Optional.swift in Sources */,
947955
D08C54B61A69A3DB00AD8286 /* Event.swift in Sources */,
948956
D0C312D319EF2A5800984962 /* Disposable.swift in Sources */,
957+
4AC73ECB1DF273570004EC4F /* ResultExtensions.swift in Sources */,
949958
EBCC7DBC1BBF010C00A2AE92 /* Observer.swift in Sources */,
950959
D03B4A3D19F4C39A009E02AC /* FoundationExtensions.swift in Sources */,
951960
9A090C141DA0309E00EE97CA /* Reactive.swift in Sources */,
@@ -997,6 +1006,7 @@
9971006
D8E84A671B3B32FB00C3E831 /* Optional.swift in Sources */,
9981007
D0C312D419EF2A5800984962 /* Disposable.swift in Sources */,
9991008
D08C54B91A69A9D100AD8286 /* SignalProducer.swift in Sources */,
1009+
4AC73ECC1DF273570004EC4F /* ResultExtensions.swift in Sources */,
10001010
9ABCB1861D2A5B5A00BCA243 /* Deprecations+Removals.swift in Sources */,
10011011
EBCC7DBD1BBF01E100A2AE92 /* Observer.swift in Sources */,
10021012
9A090C151DA0309E00EE97CA /* Reactive.swift in Sources */,

Sources/ResultExtensions.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Result
2+
3+
/// Private alias of the free `materialize()` from `Result`.
4+
///
5+
/// This exists because within a `Signal` or `SignalProducer` operator,
6+
/// `materialize()` refers to the operator with that name.
7+
/// Namespacing as `Result.materialize()` doesn't work either,
8+
/// because it tries to resolve a static member on the _type_
9+
/// `Result`, rather than the free function in the _module_
10+
/// of the same name.
11+
internal func materialize<T>(_ f: () throws -> T) -> Result<T, AnyError> {
12+
return materialize(try f())
13+
}

Sources/Signal.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2229,4 +2229,71 @@ extension SignalProtocol where Error == NoError {
22292229
.promoteErrors(NewError.self)
22302230
.timeout(after: interval, raising: error, on: scheduler)
22312231
}
2232+
2233+
/// Apply a failable `operation` to values from `self` with successful
2234+
/// results forwarded on the returned signal and thrown errors sent as
2235+
/// failed events.
2236+
///
2237+
/// - parameters:
2238+
/// - operation: A failable closure that accepts a value.
2239+
///
2240+
/// - returns: A signal that forwards successes as `value` events and thrown
2241+
/// errors as `failed` events.
2242+
public func attempt(_ operation: @escaping (Value) throws -> Void) -> Signal<Value, AnyError> {
2243+
return self
2244+
.promoteErrors(AnyError.self)
2245+
.attempt(operation)
2246+
}
2247+
2248+
/// Apply a failable `operation` to values from `self` with successful
2249+
/// results mapped on the returned signal and thrown errors sent as
2250+
/// failed events.
2251+
///
2252+
/// - parameters:
2253+
/// - operation: A failable closure that accepts a value and attempts to
2254+
/// transform it.
2255+
///
2256+
/// - returns: A signal that sends successfully mapped values from `self`, or
2257+
/// thrown errors as `failed` events.
2258+
public func attemptMap<U>(_ operation: @escaping (Value) throws -> U) -> Signal<U, AnyError> {
2259+
return self
2260+
.promoteErrors(AnyError.self)
2261+
.attemptMap(operation)
2262+
}
2263+
}
2264+
2265+
extension SignalProtocol where Error == AnyError {
2266+
/// Apply a failable `operation` to values from `self` with successful
2267+
/// results forwarded on the returned signal and thrown errors sent as
2268+
/// failed events.
2269+
///
2270+
/// - parameters:
2271+
/// - operation: A failable closure that accepts a value.
2272+
///
2273+
/// - returns: A signal that forwards successes as `value` events and thrown
2274+
/// errors as `failed` events.
2275+
public func attempt(_ operation: @escaping (Value) throws -> Void) -> Signal<Value, AnyError> {
2276+
return attemptMap { value in
2277+
try operation(value)
2278+
return value
2279+
}
2280+
}
2281+
2282+
/// Apply a failable `operation` to values from `self` with successful
2283+
/// results mapped on the returned signal and thrown errors sent as
2284+
/// failed events.
2285+
///
2286+
/// - parameters:
2287+
/// - operation: A failable closure that accepts a value and attempts to
2288+
/// transform it.
2289+
///
2290+
/// - returns: A signal that sends successfully mapped values from `self`, or
2291+
/// thrown errors as `failed` events.
2292+
public func attemptMap<U>(_ operation: @escaping (Value) throws -> U) -> Signal<U, AnyError> {
2293+
return attemptMap { value in
2294+
ReactiveSwift.materialize {
2295+
try operation(value)
2296+
}
2297+
}
2298+
}
22322299
}

Sources/SignalProducer.swift

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,82 @@ extension SignalProducerProtocol where Error == NoError {
12271227
.promoteErrors(NewError.self)
12281228
.then(replacement)
12291229
}
1230+
1231+
/// Apply a failable `operation` to values from `self` with successful
1232+
/// results forwarded on the returned producer and thrown errors sent as
1233+
/// failed events.
1234+
///
1235+
/// - parameters:
1236+
/// - operation: A failable closure that accepts a value.
1237+
///
1238+
/// - returns: A producer that forwards successes as `value` events and thrown
1239+
/// errors as `failed` events.
1240+
public func attempt(_ operation: @escaping (Value) throws -> Void) -> SignalProducer<Value, AnyError> {
1241+
return lift { $0.attempt(operation) }
1242+
}
1243+
1244+
/// Apply a failable `operation` to values from `self` with successful
1245+
/// results mapped on the returned producer and thrown errors sent as
1246+
/// failed events.
1247+
///
1248+
/// - parameters:
1249+
/// - operation: A failable closure that accepts a value and attempts to
1250+
/// transform it.
1251+
///
1252+
/// - returns: A producer that sends successfully mapped values from `self`,
1253+
/// or thrown errors as `failed` events.
1254+
public func attemptMap<U>(_ operation: @escaping (Value) throws -> U) -> SignalProducer<U, AnyError> {
1255+
return lift { $0.attemptMap(operation) }
1256+
}
1257+
}
1258+
1259+
extension SignalProducerProtocol where Error == AnyError {
1260+
/// Create a `SignalProducer` that will attempt the given failable operation once for
1261+
/// each invocation of `start()`.
1262+
///
1263+
/// Upon success, the started producer will send the resulting value then
1264+
/// complete. Upon failure, the started signal will fail with the error that
1265+
/// occurred.
1266+
///
1267+
/// - parameters:
1268+
/// - operation: A failable closure.
1269+
///
1270+
/// - returns: A `SignalProducer` that will forward a success as a `value`
1271+
/// event and then complete or `failed` event if the closure throws.
1272+
public static func attempt(_ operation: @escaping () throws -> Value) -> SignalProducer<Value, Error> {
1273+
return .attempt {
1274+
ReactiveSwift.materialize {
1275+
try operation()
1276+
}
1277+
}
1278+
}
1279+
1280+
/// Apply a failable `operation` to values from `self` with successful
1281+
/// results forwarded on the returned producer and thrown errors sent as
1282+
/// failed events.
1283+
///
1284+
/// - parameters:
1285+
/// - operation: A failable closure that accepts a value.
1286+
///
1287+
/// - returns: A producer that forwards successes as `value` events and thrown
1288+
/// errors as `failed` events.
1289+
public func attempt(_ operation: @escaping (Value) throws -> Void) -> SignalProducer<Value, AnyError> {
1290+
return lift { $0.attempt(operation) }
1291+
}
1292+
1293+
/// Apply a failable `operation` to values from `self` with successful
1294+
/// results mapped on the returned producer and thrown errors sent as
1295+
/// failed events.
1296+
///
1297+
/// - parameters:
1298+
/// - operation: A failable closure that accepts a value and attempts to
1299+
/// transform it.
1300+
///
1301+
/// - returns: A producer that sends successfully mapped values from `self`,
1302+
/// or thrown errors as `failed` events.
1303+
public func attemptMap<U>(_ operation: @escaping (Value) throws -> U) -> SignalProducer<U, AnyError> {
1304+
return lift { $0.attemptMap(operation) }
1305+
}
12301306
}
12311307

12321308
extension SignalProducerProtocol where Value: Equatable {

Tests/ReactiveSwiftTests/SignalProducerSpec.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,40 @@ class SignalProducerSpec: QuickSpec {
308308
}
309309
}
310310

311+
describe("SignalProducer.attempt throws") {
312+
it("should send a successful value then complete") {
313+
let operationReturnValue = "OperationValue"
314+
315+
let signalProducer = SignalProducer
316+
.attempt { () throws -> String in
317+
operationReturnValue
318+
}
319+
320+
var error: Error?
321+
signalProducer.startWithFailed {
322+
error = $0
323+
}
324+
325+
expect(error).to(beNil())
326+
}
327+
328+
it("should send the error") {
329+
let operationError = TestError.default
330+
331+
let signalProducer = SignalProducer
332+
.attempt { () throws -> String in
333+
throw operationError
334+
}
335+
336+
var error: TestError?
337+
signalProducer.startWithFailed {
338+
error = $0.error as? TestError
339+
}
340+
341+
expect(error) == operationError
342+
}
343+
}
344+
311345
describe("startWithSignal") {
312346
it("should invoke the closure before any effects or events") {
313347
var started = false

Tests/ReactiveSwiftTests/SignalSpec.swift

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2279,7 +2279,56 @@ class SignalSpec: QuickSpec {
22792279
expect(error) == TestError.default
22802280
}
22812281
}
2282-
2282+
2283+
describe("attempt throws") {
2284+
it("should forward original values upon success") {
2285+
let (baseSignal, observer) = Signal<Int, AnyError>.pipe()
2286+
let signal = baseSignal.attempt { _ in
2287+
_ = try operation(value: 1)
2288+
}
2289+
2290+
var current: Int?
2291+
signal
2292+
.assumeNoErrors()
2293+
.observeValues { value in
2294+
current = value
2295+
}
2296+
2297+
for value in 1...5 {
2298+
observer.send(value: value)
2299+
expect(current) == value
2300+
}
2301+
}
2302+
2303+
it("should error if an attempt fails") {
2304+
let (baseSignal, observer) = Signal<Int, NoError>.pipe()
2305+
let signal = baseSignal.attempt { _ in
2306+
_ = try operation(value: nil) as Int
2307+
}
2308+
2309+
var error: TestError?
2310+
signal.observeFailed { err in
2311+
error = err.error as? TestError
2312+
}
2313+
2314+
observer.send(value: 42)
2315+
expect(error) == TestError.default
2316+
}
2317+
2318+
it("should allow throwing closures with NoError") {
2319+
let (baseSignal, observer) = Signal<Int, NoError>.pipe()
2320+
let signal = baseSignal.attempt { _ in
2321+
_ = try operation(value: 1)
2322+
}
2323+
2324+
var value: Int?
2325+
signal.observeResult { value = $0.value }
2326+
2327+
observer.send(value: 42)
2328+
expect(value) == 42
2329+
}
2330+
}
2331+
22832332
describe("attemptMap") {
22842333
it("should forward mapped values upon success") {
22852334
let (baseSignal, observer) = Signal<Int, TestError>.pipe()
@@ -2316,7 +2365,57 @@ class SignalSpec: QuickSpec {
23162365
expect(error) == TestError.default
23172366
}
23182367
}
2319-
2368+
2369+
describe("attemptMap throws") {
2370+
it("should forward mapped values upon success") {
2371+
let (baseSignal, observer) = Signal<Int, AnyError>.pipe()
2372+
let signal = baseSignal.attemptMap { num -> Bool in
2373+
try operation(value: num % 2 == 0)
2374+
}
2375+
2376+
var even: Bool?
2377+
signal
2378+
.assumeNoErrors()
2379+
.observeValues { value in
2380+
even = value
2381+
}
2382+
2383+
observer.send(value: 1)
2384+
expect(even) == false
2385+
2386+
observer.send(value: 2)
2387+
expect(even) == true
2388+
}
2389+
2390+
it("should error if a mapping fails") {
2391+
let (baseSignal, observer) = Signal<Int, AnyError>.pipe()
2392+
let signal = baseSignal.attemptMap { _ -> Bool in
2393+
try operation(value: nil)
2394+
}
2395+
2396+
var error: TestError?
2397+
signal.observeFailed { err in
2398+
error = err.error as? TestError
2399+
}
2400+
2401+
observer.send(value: 42)
2402+
expect(error) == TestError.default
2403+
}
2404+
2405+
it("should allow throwing closures with NoError") {
2406+
let (baseSignal, observer) = Signal<Int, NoError>.pipe()
2407+
let signal = baseSignal.attemptMap { num in
2408+
try operation(value: num % 2 == 0)
2409+
}
2410+
2411+
var value: Bool?
2412+
signal.observeResult { value = $0.value }
2413+
2414+
observer.send(value: 2)
2415+
expect(value) == true
2416+
}
2417+
}
2418+
23202419
describe("combinePrevious") {
23212420
var observer: Signal<Int, NoError>.Observer!
23222421
let initialValue: Int = 0
@@ -2610,3 +2709,8 @@ class SignalSpec: QuickSpec {
26102709
}
26112710
}
26122711
}
2712+
2713+
private func operation<T>(value: T?) throws -> T {
2714+
guard let value = value else { throw TestError.default }
2715+
return value
2716+
}

0 commit comments

Comments
 (0)