8

I have a quick question:

  • I have a network request that returns Observable<Result<String, RequestError>>, let’s call it requestToken
  • if this request succeeds, I want to use the String (token) to do another request that returns Observable<Result<NSDictionary, RequestError>>, let’s call it requestData
  • when that second request comes back, I wanna merge the token into its dictionary
  • in the end I wanna map from Observable<Result<String, RequestError>> to Observable<Result<NSDictionary, RequestError>>

How can I achieve that without multiple nested levels in my code?

This is what I have today:

requestToken()
    .flatMap({ result -> Observable<Result<NSDictionary, RequestError>> in
        switch result {
        case .success(let token):
            return requestData(token: token).map({ $0.map({ $0 + ["token": token] }) })
        case .failure(let error):
            return Observable.of(.failure(error))
        }
    })
Rodrigo Ruiz
  • 4,248
  • 6
  • 43
  • 75
  • Is there a reason you are manually returning error results instead of using the built in error system? – Daniel T. Feb 24 '17 at 02:34
  • For the type system. I need to handle the `RequestError` and the error system from `Observable` will only give me a generic `Error`. Same for the `throw` syntax, none of them give me documentation of which error can happen on the function signature. – Rodrigo Ruiz Feb 24 '17 at 07:46

2 Answers2

9

Updated:

It's a detailed example, hope this may help:

enum RequestError: Error {
    case unknown
}

func requestToken() -> Observable<String> {

    return Observable.create { observer in

        let success = true

        if success {
            observer.onNext("MyTokenValue")
            observer.onCompleted()
        } else {
            observer.onError(RequestError.unknown)
        }

        return Disposables.create()
    }
}

func requestData(token: String) -> Observable<[String: Any]> {

    return Observable<[String: Any]>.create { observer in

        let success = false

        if success {
            observer.onNext(["uid": 007])
            observer.onCompleted()
        } else {
            observer.onError(RequestError.unknown)
        }

        return Disposables.create()
    }
    .map { (data: [String: Any]) in
        var newData = data
        newData["token"] = token
        return newData
    }
}


requestToken()                      // () -> Observable<String>
    .flatMapLatest(requestData)     // Observable<String> -> Observable<[String: Any]>
    .materialize()                  // Observable<[String: Any]> -> Observable<Event<[String: Any]>>
    .subscribe(onNext: { event in
        switch event {
        case .next(let dictionary):
            print("onNext:", dictionary)
        case .error(let error as RequestError):
            print("onRequestError:", error)
        case .error(let error):
            print("onOtherError:", error)
        case .completed:
            print("onCompleted")
        }
    })
    .disposed(by: disposeBag)

Original:

I think it's much easier to achieve it using materialize() with less extra work:

func requestToken() -> Observable<String> { return .empty() }
func requestData(token: String) -> Observable<NSDictionary> { return .empty() }
enum RequestError: Error {}

requestToken()
    .flatMapLatest(requestData)
    .materialize()
    .subscribe(onNext: { event in
        switch event {
        case .next(let dictionary):
            print("onNext:", dictionary)
        case .error(let error as RequestError):
            print("onRequestError:", error)
        case .error(let error):
            print("onOtherError:", error)
        case .completed:
            print("onCompleted")
        }
    })
    .disposed(by: disposeBag)

Hope this may help.

beeth0ven
  • 1,857
  • 15
  • 18
  • I couldn't find `materialize()` on my Observable. Is it present on the 3.0 version? – Rodrigo Ruiz May 26 '17 at 19:16
  • Yes it's available since version 3.4.0 https://github.com/ReactiveX/RxSwift/releases/tag/3.4.0 – beeth0ven May 29 '17 at 05:19
  • I'm not sure I understand what `materialize` is for, first, my `requestToken` returns a `Result`, not a `String`, so I can't `.flatMapLatest(requestData)`. Second, I need the token merged with the resulting dictionary of `requestData `. Could you clarify? – Rodrigo Ruiz May 30 '17 at 14:55
  • Yes, you see `requestToken` returns a `Result`, It's difficult to `flatMap` with a `Result`. So my code shows you need to change `requestToken` to return `Observable` instead of `Observable>`. So my code's flow is `() -> Observable -> Observable -> Observable>`. And `Event` is something like `Result` which has three case `next `, `error`, `completed`. What `materialize()` dose is change `Observable` to `Observable>`. So you can subscribe to the it. – beeth0ven May 31 '17 at 01:44
  • But then how will I know if `requestToken` can fail if it's saying it always returns a String? – Rodrigo Ruiz Jun 01 '17 at 21:03
1

If you use the built in error system, you can save yourself from having to manually pass the error along and all the switches that would entail. You can cast the error at the end.

I would do something more like this:

// this is necessary to handle adding the token to the dictionary.
extension Dictionary {

    /// An immutable version of update. Returns a new dictionary containing self's values and the key/value passed in.
    func updatedValue(_ value: Value, forKey key: Key) -> Dictionary<Key, Value> {
        var result = self
        result[key] = value
        return result
    }
}

// function signatures, note that they don't return Results anymore.
func requestToken() -> Observable<String> { /*...*/ }
func requestData(withToken: String) -> Observable<[String: Any]> { /*...*/ }

requestToken().flatMapLatest {
    requestData(token: $0)
        .map { $0.updatedValue($0, forKey: "token") }
        .map { .success($0) }
}.catchError {
        Observable.just(.failure($0 as! RequestError))
}

With the above, the end result would be an Observable<Result<[String: Any], RequestError>> just like in your case, but the error handling is much cleaner.

If you can't change the signatures of the two functions you are using then I would do this:

    func throwError<T, U: Error>(result: Result<T, U>) throws -> T {
        switch result {
        case .success(let token):
            return token
        case .failure(let error):
            throw error
        }
    }

    requestToken().map {
        try throwError(result: $0)
    }.flatMapLatest {
        requestData(token: $0)
            .map { try throwError(result: $0) }
            .map { $0.updatedValue($0, forKey: "token") }
    }
    .map { .success($0) }
    .catchError {
        Observable.just(.failure($0 as! RequestError))
    }
Daniel T.
  • 32,821
  • 6
  • 50
  • 72