18

I use an OAuth framework which creates authenticated requests asynchronously like so:

OAuthSession.current.makeAuthenticatedRequest(request: myURLRequest) { (result: Result<URLRequest, OAuthError>) in
            switch result {
            case .success(let request):
                URLSession.shared.dataTask(with: request) { (data, response, error) in
                    // ...
                }
             // ...
             }
        }

I am trying to make my OAuth framework use Combine, so I know have a Publisher version of the makeAuthenticatedRequest method i.e.:

public func makeAuthenticatedRequest(request: URLRequest) -> AnyPublisher<URLRequest, OAuthError>

I am trying to use this to replace the call site above like so:

OAuthSession.current.makeAuthenticatedRequestPublisher(request)
    .tryMap(URLSession.shared.dataTaskPublisher(for:))
    .tryMap { (data, _) in data } // Problem is here
    .decode(type: A.self, decoder: decoder)

As noted above, the problem is on turning the result of the publisher into a new publisher. How can I go about doing this?

jjatie
  • 5,152
  • 5
  • 34
  • 56

1 Answers1

43

You need to use flatMap, not tryMap, around dataTaskPublisher(for:).

Look at the types. Start with this:

let p0 = OAuthSession.current.makeAuthenticatedRequest(request: request)

Option-click on p0 to see its deduced type. It is AnyPublisher<URLRequest, OAuthError>, since that is what makeAuthenticatedRequest(request:) is declared to return.

Now add this:

let p1 = p0.tryMap(URLSession.shared.dataTaskPublisher(for:))

Option-click on p1 to see its deduced type, Publishers.TryMap<AnyPublisher<URLRequest, OAuthError>, URLSession.DataTaskPublisher>. Oops, that's a little hard to understand. Simplify it by using eraseToAnyPublisher:

let p1 = p0
    .tryMap(URLSession.shared.dataTaskPublisher(for:))
    .eraseToAnyPublisher()

Now the deduced type of p1 is AnyPublisher<URLSession.DataTaskPublisher, Error>. That still has the somewhat mysterious type URLSession.DataTaskPublisher in it, so let's erase that too:

let p1 = p0.tryMap {
    URLSession.shared.dataTaskPublisher(for: $0)
        .eraseToAnyPublisher() }
    .eraseToAnyPublisher()

Now Xcode can tell us that the deduced type of p1 is AnyPublisher<AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>, OAuthError>. Let me reformat that for readability:

AnyPublisher<
    AnyPublisher<
        URLSession.DataTaskPublisher.Output, 
        URLSession.DataTaskPublisher.Failure>,
    OAuthError>

It's a publisher that publishes publishers that publish URLSession.DataTaskPublisher.Output.

That's not what you expected, and it's why your second tryMap fails. You thought you were creating a publisher of URLSession.DataTaskPublisher.Output (which is a typealias for the tuple (data: Data, response: URLResponse)), and that's the input your second tryMap wants. But Combine thinks your second tryMap's input should be a URLSession.DataTaskPublisher.

When you see this kind of nesting, with a publisher that publishes publishers, it means you probably needed to use flatMap instead of map (or tryMap). Let's do that:

let p1 = p0.flatMap {
       //   ^^^^^^^ flatMap instead of tryMap
    URLSession.shared.dataTaskPublisher(for: $0)
        .eraseToAnyPublisher() }
    .eraseToAnyPublisher()

Now we get a compile-time error:

Instance method 'flatMap(maxPublishers:_:)' requires the types 'OAuthError' and 'URLSession.DataTaskPublisher.Failure' (aka 'URLError') be equivalent

The problem is that Combine can't flatten the nesting because the outer publisher's failure type is OAuthError and the inner publisher's failure type is URLError. Combine can only flatten them if they have the same failure type. We can fix this problem by converting both failure types to the general Error type:

let p1 = p0
    .mapError { $0 as Error }
    .flatMap {
        URLSession.shared.dataTaskPublisher(for: $0)
            .mapError { $0 as Error }
            .eraseToAnyPublisher() }
    .eraseToAnyPublisher()

This compiles, and Xcode tells us that the deduced type is AnyPublisher<URLSession.DataTaskPublisher.Output, Error>, which is what we want. We can tack on your next tryMap, but let's just use map instead because the body can't throw any errors:

let p2 = p1.map { $0.data }.eraseToAnyPublisher()

Xcode tells us p2 is an AnyPublisher<Data, Error>, so we could then chain a decode modifier.

Now that we have straightened out the types, we can get rid of all the type erasers and put it all together:

OAuthSession.current.makeAuthenticatedRequest(request: request)
    .mapError { $0 as Error }
    .flatMap {
        URLSession.shared.dataTaskPublisher(for: $0)
            .mapError { $0 as Error } }
    .map { $0.data }
    .decode(type: A.self, decoder: decoder)
rob mayoff
  • 375,296
  • 67
  • 796
  • 848