RxSwiftCommunity / RxSwiftExt

A collection of Rx operators & tools not found in the core RxSwift distribution
MIT License
1.33k stars 213 forks source link

Proposal for a `Single<Value>.asResult()` operator #223

Closed DivineDominion closed 5 years ago

DivineDominion commented 5 years ago

Name and description

Operator signature:

Single<Element>.asResult() -> Result<Element, F>

Tries to cast Event.error to Result.Failure and emits the event as .next(.failure(...)) instead of exiting the sequence with an error. If the cast fails, continue with whatever error event happened.

Implementation

extension RxSwift.PrimitiveSequence where Trait == RxSwift.SingleTrait {
    /// Keeps success values, and tries to recover from `.error` with `Result.failure` of the specified type.
    /// Passes on `.error` if the transformation fails.
    internal func asResult<Failure>() -> Observable<Result<Element, Failure>> where Failure: Swift.Error {
        return self.asObservable()
            .map { Result<Element, Failure>.success($0) }
            .catchError(attemptErrorCastToFailure)
    }
}

fileprivate func attemptErrorCastToFailure<Element, Failure>(error: Error)
    -> Observable<Result<Element, Failure>>
    where Failure: Error
{
    guard let failure = error as? Failure else {
        return .error(error)
    }
    return .just(.failure(failure))
}

Motivation for inclusion

The RxSwift docs on Traits provide HTTP requests an an example for Single<Value>. If you produce .error for failed requests, this will end any related observable sequence with error as well; you need to recover from errors in this case:

// Given a UI element that produces search requests
let requests = textField.rx.text.shared()
let disposeBag = DisposeBag()

// Given this HTTP API request function
func getResults(text: String) -> Single<[String]> { /* ... */ }

// When you map requests to requests results, the requests sequence will end on error
requests
    .map(getResult(text:))
    .switchLatest()
    .subscribe {
        switch $0 {
        case .success(let results):
            print(results)
        case .error(let error):
            print("Failed to perform requests: \(error)")
        }
    }.disposed(by: disposeBag)

Usually, you'll want to retry, catch errors and alert the user, or otherwise recover from the situation. You don't want the whole wiring to fall apart.

Since Single<Value> is so close to Result<Success, Failure>, I figured that expected errors can be mapped to Result.failures. This works when your HTTP API produces 1 error type.

Example of use

The motivation example above, transformed to

// Given a UI element that produces search requests
let requests = textField.rx.text.shared()
let disposeBag = DisposeBag()

// Given this HTTP API request function
func getResults(text: String) -> Single<[String]> { /* ... */ }

// When you map requests to requests results, the requests sequence will end on error
requests
    .map { getResult(text: $0).asResult() }
    .switchLatest()
    // .catchErrorJustReturn([]) // Maybe handle other errors separately
    .subscribe(onNext: {
        switch $0 {
        case .success(let results):
            print(results)
        case .failure(let failure):
            // Handle expected API failures here without aborting the sequence:
            print("Failed to perform requests: \(failure)")
        }
    }).disposed(by: disposeBag)
M0rtyMerr commented 5 years ago

Hi @DivineDominion thank you for contribution! I think it's not generic enough to be included in RxSwiftExt. Moreover, I would say that using Result instead of raw events is not very good idea, because you are actually breaking the contract. RxSwift best practice - use builtin error system and follow the contract. You can read more about it here - https://github.com/ReactiveX/RxSwift/issues/729

freak4pc commented 5 years ago

I don't know if it's a good idea or not to use Result, but I agree with Anton that it's not a common enough use case to add into RxSwiftExt IMHO. If you could gather some substantial community interest around this, we can explore this further :) Thanks!

M0rtyMerr commented 5 years ago

Closed due to lack of arguments for adding this