Combine turn one Publisher into another

两盒软妹~` 提交于 2021-01-21 04:08:10

问题


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?


回答1:


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)


来源:https://stackoverflow.com/questions/57153221/combine-turn-one-publisher-into-another

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!