Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ All operators, utilities and helpers respect Combine's publisher contract, inclu
* [nwise(_:) and pairwise()](#nwise)
* [ignoreOutput(setOutputType:)](#ignoreOutputsetOutputType)
* [ignoreFailure](#ignoreFailure)
* [mapToResult](#mapToResult)
* [flatMapBatches(of:)](#flatMapBatchesof)

### Publishers
Expand Down Expand Up @@ -697,6 +698,41 @@ subject.send(completion: .failure(.someError))
3
.finished
```
------

### mapToResult

Transforms a publisher of type `AnyPublisher<Output, Failure>` to `AnyPublisher<Result<Output, Failure>, Never>`

```swift
enum AnError: Error {
case someError
}

let subject = PassthroughSubject<Int, AnError>()

let subscription = subject
.mapToResult()
.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
```

------

### flatMapBatches(of:)

Expand Down
25 changes: 25 additions & 0 deletions Sources/Operators/MapToResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// MapToResult.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 {
/// 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 <Result<Output, Failure>, Never>
func mapToResult() -> AnyPublisher<Result<Output, Failure>, Never> {
map(Result.success)
.catch { Just(.failure($0)) }
.eraseToAnyPublisher()
}
}

#endif
155 changes: 155 additions & 0 deletions Tests/MapToResultTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//
// MapToResultTests.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 MapToResultTests: XCTestCase {
private var subscription: AnyCancellable!

enum MapToResultError: Error {
case someError
}

func testMapResultNoError() {
let subject = PassthroughSubject<Int, Error>()
let testInt = 5
var completed = false
var results: [Result<Int, Error>] = []

subscription = subject
.mapToResult()
.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 testMapCustomError() {
let subject = PassthroughSubject<Int, Error>()
var completed = false
var gotFailure = false
var gotSuccess = false
var result: Result<Int, Error>? = nil

subscription = subject
.tryMap { _ -> Int in throw MapToResultError.someError }
.mapToResult()
.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<Data, Error>()
var completed = false
var gotFailure = false
var gotSuccess = false
var result: Result<ToDecode, Error>? = nil

subscription = subject
.decode(type: ToDecode.self, decoder: JSONDecoder())
.mapToResult()
.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 testMapEncodeError() {
struct ToEncode: Encodable {
let foo: Int

func encode(to encoder: Encoder) throws {
throw EncodingError.invalidValue((), EncodingError.Context(codingPath: [], debugDescription: String()))
}
}

let subject = PassthroughSubject<ToEncode, Error>()
var completed = false
var gotFailure = false
var gotSuccess = false
var result: Result<Data, Error>? = nil

subscription = subject
.encode(encoder: JSONEncoder())
.mapToResult()
.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