1

I'm new to the Combine game and am trying to figure out how to generalize a HTTP POST request.

I created the following APIService class to extend individual resource services from:

import Foundation
import Combine

class APIService {
  let decoder: JSONDecoder
  let session: URLSession
  
  init(session: URLSession = URLSession.shared, decoder: JSONDecoder = JSONDecoder()) {
    self.decoder = decoder
    self.session = session
  }
}

// MARK: JSON API
extension APIService {
  struct Response<T> {
    let response: HTTPURLResponse
    let value: T
  }
  
  func post<T: Codable>(
    payload: T,
    url: URL
  ) -> AnyPublisher<Response<T>, APIError> {
    return Just(payload)
      .setFailureType(to: APIError.self) // <<< THIS WAS MISSING!
      .encode(encoder: JSONEncoder())
      .flatMap({ [weak self] payload -> AnyPublisher<Data, Error> in
        guard let self = self else {
          return Fail(error: .default("Failing to establish self.")).eraseToAnyPublisher()
        }
        var request = URLRequest(url: url)
        request.httpMethod = Methods.post
        request.setValue(
          Mimetypes.json,
          forHTTPHeaderField: Headers.contentType
        )
        request.httpBody = payload
        return self.session
          .dataTaskPublisher(
            for: request
          )
          .tryMap { response -> Response<T> in
            let value = try self.decoder.decode(T.self, from: response.data)
            return Response(
              value: value,
              response: response.response
            )
          }
          .mapError { error in
            return APIError.default(error.localizedDescription)
          }
          .receive(on: DispatchQueue.main)
          .eraseToAnyPublisher()
      })
      .eraseToAnyPublisher()
  }
}

However, this class won't compile with the following error at the post function.

Type of expression is ambiguous without more context

Being new to Swift in general and Combine in particular, I am unfortunately out of ideas on how to proceed.

Any help is greatly appreciated!

Figured it out myself: Solution

Add a Failure type to the Just, so input and output Failure types to flatMap are equal. Or put differently: flatMap cannot convert the Never failing Just to a Publisher with Failure.

The missing line in my case:

Just(payload)
  .setFailureType(to: APIError.self)
Haensl
  • 343
  • 3
  • 16

2 Answers2

1

You just have a few compile-time mistakes, which the Swift type inference system isn't able to pinpoint when they happen within a notoriously cranky flatMap Combine operator.

  1. First, you're using the wrong order of parameters and the type of URLResponse, in creating a Response object. Correct it to:
return Response(
   response: response.response as! HTTPURLResponse,
   value: value
)
  1. Second, your flatMap is not actually returning AnyPublisher<Data, Error> - the return type you specified inside its closure. The return type is AnyPublisher<Response<T>, APIError>. So, you can change that, but then you'll run into another problem, which is that the Error type of flatMap has to be the same as its upstream, which currently is not APIError, so I'd suggest just moving the mapError out of flatMap. It would look like this:
return Just(payload)
    .encode(encoder: JSONEncoder())
    .flatMap({ [weak self] payload -> AnyPublisher<Response<T>, Error> in
        guard let self = self else {
            return Fail(error: APIError.default("...")).eraseToAnyPublisher()
        }
        var request = URLRequest(url: url)
        request.httpMethod = "Methods.post"
        request.setValue(
            Mimetypes.json,
            forHTTPHeaderField: Headers.contentType
        )
        request.httpBody = payload
        return self.session
            .dataTaskPublisher(
                for: request
            )
            .tryMap { response -> Response<T> in
                let value = try self.decoder.decode(T.self, from: response.data)
                return Response(
                    response: response.response as! HTTPURLResponse,
                    value: value
                    )
            }
            .eraseToAnyPublisher()
            
    })
    .mapError { error in
        return APIError.default(error.localizedDescription)
    }
    .receive(on: DispatchQueue.main)
    .eraseToAnyPublisher()
New Dev
  • 48,427
  • 12
  • 87
  • 129
0

Figured it out, thanks to this solid guide to debugging Publishers on another question.

The Just needs to be augmented with a Failure, because flatMap needs the Failure of input and output streams to be the same.

We can use setFailureType(to: <T>) to do so. I have updated my question to reflect this solution.

Haensl
  • 343
  • 3
  • 16