From 8ce482f6a539fc7ee57687ec8693aabff3b86155 Mon Sep 17 00:00:00 2001 From: Yurii Zadoianchuk Date: Fri, 5 Mar 2021 14:47:10 +0100 Subject: [PATCH 1/4] Add catchToResult operator, tests --- Sources/Operators/CatchToResult.swift | 25 +++++ Tests/CatchToResultTests.swift | 153 ++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 Sources/Operators/CatchToResult.swift create mode 100644 Tests/CatchToResultTests.swift diff --git a/Sources/Operators/CatchToResult.swift b/Sources/Operators/CatchToResult.swift new file mode 100644 index 0000000..6c66c4d --- /dev/null +++ b/Sources/Operators/CatchToResult.swift @@ -0,0 +1,25 @@ +// +// CatchToResult.swift +// CombineExt +// +// Created by Yurii Zadoianchuk on 05/03/2021. +// Copyright © 2021 Combine Community. All rights reserved. +// + +#if canImport(Combine) +import Combine + +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public extension Publisher where Failure: Error { + /// Transform a publisher with concrete Output and Failure types + /// to a new publisher that wraps Output and Failure in Result, + /// and has Never for Failure type + /// - Returns: A type-erased publiser of type , Never> + func catchToResult() -> AnyPublisher, Never> { + self.map({ Result.success($0) }) + .catch({ Just(.failure($0)) }) + .eraseToAnyPublisher() + } +} + +#endif diff --git a/Tests/CatchToResultTests.swift b/Tests/CatchToResultTests.swift new file mode 100644 index 0000000..3034ff5 --- /dev/null +++ b/Tests/CatchToResultTests.swift @@ -0,0 +1,153 @@ +// +// CatchToResult.swift +// CombineExt +// +// Created by Yurii Zadoianchuk on 05/03/2021. +// Copyright © 2021 Combine Community. All rights reserved. +// + +import Foundation + +#if !os(watchOS) +import XCTest +import Combine +import CombineExt + +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +final class CatchToResultTests: XCTestCase { + private var subscription: AnyCancellable! + + enum CatchToResultError: Error { + case someError + } + + func testCatchToResult() { + let subject = PassthroughSubject() + let testInt = 5 + var completed = false + var results: [Result] = [] + + subscription = subject + .catchToResult() + .sink(receiveCompletion: { _ in completed = true }, + receiveValue: { results.append($0) }) + + subject.send(testInt) + XCTAssertFalse(completed) + subject.send(testInt) + subject.send(completion: .finished) + XCTAssertTrue(completed) + XCTAssertEqual(results.count, 2) + let intsCorrect = results.compactMap({ try? $0.get() }).allSatisfy({ $0 == testInt }) + XCTAssertTrue(intsCorrect) + } + + func testCatchCustomError() { + let subject = PassthroughSubject() + var completed = false + var gotFailure = false + var gotSuccess = false + var result: Result? = nil + + subscription = subject + .tryMap({ _ -> Int in throw(CatchToResultError.someError) }) + .catchToResult() + .eraseToAnyPublisher() + .sink(receiveCompletion: { _ in completed = true }, + receiveValue: { result = $0 }) + + subject.send(0) + XCTAssertNotNil(result) + + do { + _ = try result!.get() + gotSuccess = true + } catch { + gotFailure = true + } + + XCTAssertTrue(gotFailure) + XCTAssertFalse(gotSuccess) + XCTAssertTrue(completed) + } + + func testCatchDecodeError() { + struct ToDecode: Decodable { + let foo: Int + } + + let incorrectJson = """ + { + "foo": "1" + } + """ + + let subject = PassthroughSubject() + var completed = false + var gotFailure = false + var gotSuccess = false + var result: Result? = nil + + subscription = subject + .decode(type: ToDecode.self, decoder: JSONDecoder()) + .catchToResult() + .eraseToAnyPublisher() + .sink(receiveCompletion: { _ in completed = true }, + receiveValue: { result = $0 }) + + subject.send(incorrectJson.data(using: .utf8)!) + XCTAssertNotNil(result) + + do { + _ = try result!.get() + gotSuccess = true + } catch (let e) { + XCTAssert(e is DecodingError) + gotFailure = true + } + + XCTAssertTrue(gotFailure) + XCTAssertFalse(gotSuccess) + XCTAssertTrue(completed) + } + + func testCatchEncodeError() { + struct ToEncode: Encodable { + let foo: Int + + func encode(to encoder: Encoder) throws { + throw EncodingError.invalidValue((), EncodingError.Context(codingPath: [], debugDescription: String())) + } + } + + let subject = PassthroughSubject() + var completed = false + var gotFailure = false + var gotSuccess = false + var result: Result? = nil + + subscription = subject + .encode(encoder: JSONEncoder()) + .catchToResult() + .eraseToAnyPublisher() + .sink(receiveCompletion: { _ in completed = true }, + receiveValue: { result = $0 }) + + subject.send(ToEncode(foo: 0)) + XCTAssertNotNil(result) + + do { + _ = try result!.get() + gotSuccess = true + } catch (let e) { + XCTAssert(e is EncodingError) + gotFailure = true + } + + XCTAssertTrue(gotFailure) + XCTAssertFalse(gotSuccess) + XCTAssertTrue(completed) + } +} + +#endif From 91c140417e57967375c3923ba8084e99c7e499a3 Mon Sep 17 00:00:00 2001 From: Yurii Date: Sun, 7 Mar 2021 12:46:44 +0100 Subject: [PATCH 2/4] Update README.md with info about catchToResult --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index f8488ea..c9c6a96 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ All operators, utilities and helpers respect Combine's publisher contract, inclu * [nwise(_:) and pairwise()](#nwise) * [ignoreOutput(setOutputType:)](#ignoreOutputsetOutputType) * [ignoreFailure](#ignoreFailure) +* [catchToResult](#catchToResult) ### Publishers * [AnyPublisher.create](#AnypublisherCreate) @@ -682,6 +683,41 @@ subject.send(completion: .failure(.someError)) 3 .finished ``` +------ + +### catchToResult + +Transforms a publisher of type `AnyPublisher` to `AnyPublisher, Never>` + +```swift +enum AnError: Error { + case someError +} + +let subject = PassthroughSubject() + +let subscription = subject + .catchToResult() + .sink(receiveCompletion: { print("completion: \($0)") }, + receiveValue: { print("value: \($0)") }) + +subject.send(1) +subject.send(2) +subject.send(3) +subject.send(completion: .failure(.someError)) +``` + +#### Output + +```none +value: success(1) +value: success(2) +value: success(3) +value: failure(AnError.someError) +completion: finished +``` + +------ ## Publishers From 69d6c805de129fd78b458e1fcbbd4183486838a5 Mon Sep 17 00:00:00 2001 From: Yurii Zadoianchuk Date: Sun, 7 Mar 2021 12:50:27 +0100 Subject: [PATCH 3/4] Apply suggested review code-style edits --- Sources/Operators/CatchToResult.swift | 6 +++--- Tests/CatchToResultTests.swift | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/Operators/CatchToResult.swift b/Sources/Operators/CatchToResult.swift index 6c66c4d..38eb7fd 100644 --- a/Sources/Operators/CatchToResult.swift +++ b/Sources/Operators/CatchToResult.swift @@ -10,14 +10,14 @@ import Combine @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -public extension Publisher where Failure: Error { +public extension Publisher { /// Transform a publisher with concrete Output and Failure types /// to a new publisher that wraps Output and Failure in Result, /// and has Never for Failure type /// - Returns: A type-erased publiser of type , Never> func catchToResult() -> AnyPublisher, Never> { - self.map({ Result.success($0) }) - .catch({ Just(.failure($0)) }) + map(Result.success) + .catch { Just(.failure($0)) } .eraseToAnyPublisher() } } diff --git a/Tests/CatchToResultTests.swift b/Tests/CatchToResultTests.swift index 3034ff5..4e911e7 100644 --- a/Tests/CatchToResultTests.swift +++ b/Tests/CatchToResultTests.swift @@ -21,7 +21,7 @@ final class CatchToResultTests: XCTestCase { case someError } - func testCatchToResult() { + func testCatchResultNoError() { let subject = PassthroughSubject() let testInt = 5 var completed = false @@ -38,7 +38,9 @@ final class CatchToResultTests: XCTestCase { subject.send(completion: .finished) XCTAssertTrue(completed) XCTAssertEqual(results.count, 2) - let intsCorrect = results.compactMap({ try? $0.get() }).allSatisfy({ $0 == testInt }) + let intsCorrect = results + .compactMap { try? $0.get() } + .allSatisfy { $0 == testInt } XCTAssertTrue(intsCorrect) } @@ -50,7 +52,7 @@ final class CatchToResultTests: XCTestCase { var result: Result? = nil subscription = subject - .tryMap({ _ -> Int in throw(CatchToResultError.someError) }) + .tryMap { _ -> Int in throw CatchToResultError.someError } .catchToResult() .eraseToAnyPublisher() .sink(receiveCompletion: { _ in completed = true }, @@ -101,7 +103,7 @@ final class CatchToResultTests: XCTestCase { do { _ = try result!.get() gotSuccess = true - } catch (let e) { + } catch let e { XCTAssert(e is DecodingError) gotFailure = true } @@ -139,7 +141,7 @@ final class CatchToResultTests: XCTestCase { do { _ = try result!.get() gotSuccess = true - } catch (let e) { + } catch let e { XCTAssert(e is EncodingError) gotFailure = true } From bea35a1820ad117d603d4b17f278f45e759b7160 Mon Sep 17 00:00:00 2001 From: Yurii Zadoianchuk Date: Mon, 22 Mar 2021 10:02:32 +0100 Subject: [PATCH 4/4] Rename catchToResult to mapToResult --- Package.resolved | 4 ++-- README.md | 6 ++--- ...{CatchToResult.swift => MapToResult.swift} | 4 ++-- ...sultTests.swift => MapToResultTests.swift} | 22 +++++++++---------- 4 files changed, 18 insertions(+), 18 deletions(-) rename Sources/Operators/{CatchToResult.swift => MapToResult.swift} (86%) rename Tests/{CatchToResultTests.swift => MapToResultTests.swift} (90%) diff --git a/Package.resolved b/Package.resolved index ed69403..bf86c1e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", "state": { "branch": null, - "revision": "afc84b6a3639198b7b8b6d79f04eb3c2ee590d29", - "version": "0.1.1" + "revision": "ae2f434e81017bb7de02c168fb0bde83cd8370c1", + "version": "0.3.1" } } ] diff --git a/README.md b/README.md index 9f8ef52..a8415f1 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ All operators, utilities and helpers respect Combine's publisher contract, inclu * [nwise(_:) and pairwise()](#nwise) * [ignoreOutput(setOutputType:)](#ignoreOutputsetOutputType) * [ignoreFailure](#ignoreFailure) -* [catchToResult](#catchToResult) +* [mapToResult](#mapToResult) * [flatMapBatches(of:)](#flatMapBatchesof) ### Publishers @@ -700,7 +700,7 @@ subject.send(completion: .failure(.someError)) ``` ------ -### catchToResult +### mapToResult Transforms a publisher of type `AnyPublisher` to `AnyPublisher, Never>` @@ -712,7 +712,7 @@ enum AnError: Error { let subject = PassthroughSubject() let subscription = subject - .catchToResult() + .mapToResult() .sink(receiveCompletion: { print("completion: \($0)") }, receiveValue: { print("value: \($0)") }) diff --git a/Sources/Operators/CatchToResult.swift b/Sources/Operators/MapToResult.swift similarity index 86% rename from Sources/Operators/CatchToResult.swift rename to Sources/Operators/MapToResult.swift index 38eb7fd..6ad3552 100644 --- a/Sources/Operators/CatchToResult.swift +++ b/Sources/Operators/MapToResult.swift @@ -1,5 +1,5 @@ // -// CatchToResult.swift +// MapToResult.swift // CombineExt // // Created by Yurii Zadoianchuk on 05/03/2021. @@ -15,7 +15,7 @@ public extension Publisher { /// to a new publisher that wraps Output and Failure in Result, /// and has Never for Failure type /// - Returns: A type-erased publiser of type , Never> - func catchToResult() -> AnyPublisher, Never> { + func mapToResult() -> AnyPublisher, Never> { map(Result.success) .catch { Just(.failure($0)) } .eraseToAnyPublisher() diff --git a/Tests/CatchToResultTests.swift b/Tests/MapToResultTests.swift similarity index 90% rename from Tests/CatchToResultTests.swift rename to Tests/MapToResultTests.swift index 4e911e7..7ecf8a2 100644 --- a/Tests/CatchToResultTests.swift +++ b/Tests/MapToResultTests.swift @@ -1,5 +1,5 @@ // -// CatchToResult.swift +// MapToResultTests.swift // CombineExt // // Created by Yurii Zadoianchuk on 05/03/2021. @@ -14,21 +14,21 @@ import Combine import CombineExt @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -final class CatchToResultTests: XCTestCase { +final class MapToResultTests: XCTestCase { private var subscription: AnyCancellable! - enum CatchToResultError: Error { + enum MapToResultError: Error { case someError } - func testCatchResultNoError() { + func testMapResultNoError() { let subject = PassthroughSubject() let testInt = 5 var completed = false var results: [Result] = [] subscription = subject - .catchToResult() + .mapToResult() .sink(receiveCompletion: { _ in completed = true }, receiveValue: { results.append($0) }) @@ -44,7 +44,7 @@ final class CatchToResultTests: XCTestCase { XCTAssertTrue(intsCorrect) } - func testCatchCustomError() { + func testMapCustomError() { let subject = PassthroughSubject() var completed = false var gotFailure = false @@ -52,8 +52,8 @@ final class CatchToResultTests: XCTestCase { var result: Result? = nil subscription = subject - .tryMap { _ -> Int in throw CatchToResultError.someError } - .catchToResult() + .tryMap { _ -> Int in throw MapToResultError.someError } + .mapToResult() .eraseToAnyPublisher() .sink(receiveCompletion: { _ in completed = true }, receiveValue: { result = $0 }) @@ -92,7 +92,7 @@ final class CatchToResultTests: XCTestCase { subscription = subject .decode(type: ToDecode.self, decoder: JSONDecoder()) - .catchToResult() + .mapToResult() .eraseToAnyPublisher() .sink(receiveCompletion: { _ in completed = true }, receiveValue: { result = $0 }) @@ -113,7 +113,7 @@ final class CatchToResultTests: XCTestCase { XCTAssertTrue(completed) } - func testCatchEncodeError() { + func testMapEncodeError() { struct ToEncode: Encodable { let foo: Int @@ -130,7 +130,7 @@ final class CatchToResultTests: XCTestCase { subscription = subject .encode(encoder: JSONEncoder()) - .catchToResult() + .mapToResult() .eraseToAnyPublisher() .sink(receiveCompletion: { _ in completed = true }, receiveValue: { result = $0 })