Home > Net >  How to make this publisher extension generic
How to make this publisher extension generic

Time:01-04

I have the following extension on a Publisher which allows me to paginate a URL request. I originally used this in a specific use case, where the Output of the publisher was of type CustomType.

extension Publisher where Output == CustomType,
                          Failure == Error {
  func paginate(pageIdPublisher: CurrentValueSubject<String?, Never>) -> AnyPublisher<[User], Never> {
    return self
      .handleEvents(receiveOutput: { response in
        if let maxId = response.pageId {
          pageIdPublisher.send(maxId)
        } else {
          pageIdPublisher.send(completion: .finished)
        }
      })
      .reduce([]) { allUsers, response in
        return response.users   allUsers
      }
      .catch { error in
        Just([])
      }
      .eraseToAnyPublisher()
  }
}

struct CustomType: Codable {
  let users: [User]
  let pageId: String?
}

This is called like this:

func loadItem() async throws -> [String] {
  let pageIdPublisher = CurrentValueSubject<String?, Never>(nil)

  return try await pageIdPublisher
    .flatMap { pageId in
      urlSession
        .publisher(
          for: .item(pageId: pageId),
          receiveOn: queue
        )
    }
    .paginate(pageIdPublisher: pageIdPublisher) // <- This part
    .singleOutput()
}

However, I now want to make it generic so that it can be used on any Output type, so long as it has a pageId and some kind of array.

I tried using a protocol Pageable like this:

protocol Pageable {
  associatedtype T

  var pageId: String? {get}
  var items: [T] {get}
}

But I can't use that with the extension because Output can't have be used with a protocol that contains an associatedType.

Is this possible?

CodePudding user response:

If you constrained the Output type with : to your Pageable protocol, and used Output.T for the returned publisher's output type, the paginate method should compile:

extension Publisher where Output: Pageable,
                          Failure == Error {
  func paginate(pageIdPublisher: CurrentValueSubject<String?, Never>) -> AnyPublisher<[Output.T], Never> {
   return self
     .handleEvents(receiveOutput: { response in
       if let maxId = response.pageId {
         pageIdPublisher.send(maxId)
       } else {
         pageIdPublisher.send(completion: .finished)
       }
     })
     .reduce([]) { allItems, response in
       return response.items   allItems
     }
     .catch { error in
       Just([])
     }
     .eraseToAnyPublisher()
  }
}

Another idea of making this more generic would be to constrain Output to be an Identifiable type, where ID is an optional, and provide an extra closure argument to specify the mapping from Output to the array:

extension Publisher where Output: Identifiable,
                          Failure == Error {
    func paginate<T, ID>(
        pageIdPublisher: CurrentValueSubject<Output.ID?, Never>,
        items: @escaping (Output) -> [T]
    )
        -> AnyPublisher<[T], Never>
        where Self.Output.ID == ID?
    {
        ...

    }
}

You would use response.id instead of response.pageId, and items(response) instead of response.items in the implementation. At the callsite, you would pass in \.users for the closure argument, for example.

  •  Tags:  
  • Related