Какой оператор / подход комбината можно использовать для загрузки всех страниц «API с постраничным интерфейсом»? - PullRequest
1 голос
/ 16 января 2020

Я работаю с веб-API, который доставляет результаты до заданного предела (pageSize параметр запроса). Если число результатов превышает этот предел, ответное сообщение предварительно заполняется URL-адресом, по которому можно выполнить дополнительный запрос для получения большего количества результатов. Если есть еще больше результатов, это снова указывается тем же способом.

Мое намерение - получить все результаты сразу.

В настоящее время у меня есть что-то вроде следующих структур запросов и ответов:

// Request structure
struct TvShowsSearchRequest {
    let q: String
    let pageSize: Int?
}

// Response structure
struct TvShowsSearchResponse: Decodable {
    let next: String?
    let total : Int
    let searchTerm : String
    let searchResultListShow: [SearchResult]?
}

При решении проблемы ' old style ' с использованием обработчиков завершения мне пришлось написать обработчик, который вызывает запрос 'handle more' с URL-адресом ответа:

func handleResponse(request: TvShowsSearchRequest, result: Result<TvShowsSearchResponse, Error>) -> Void {
    switch result {
    case .failure(let error):
        fatalError(error.localizedDescription)
    case .success(let value):
        print("> Total number of shows matching the query: \(value.total)")
        print("> Number of shows fetched: \(value.searchResultListShow?.count ?? 0)")

        if let moreUrl = value.next {
            print("> URL to fetch more entries \(moreUrl)")

            // start recursion here: a new request, calling the same completion handler...
            dataProvider.handleMore(request, nextUrl: moreUrl, completion: handleResponse)
        }
    }
}

let request = TvShowsSearchRequest(query: "A", pageSize: 50)
dataProvider.send(request, completion: handleResponse)

Внутренне функции send и handleMore вызывают один и тот же internalSend, который принимает request и url, для последующего вызова URLSession.dataTask(...), проверки на наличие ошибок HTTP, декодирования ответа и вызова блока завершения.

Теперь я хочу использовать платформу Combine и использовать Publisher, который автоматически предоставляет постраничные ответы, без необходимости вызова другого Publisher.

Поэтому я написал requestPublisher функцию, которая принимает request и (начальный) url и возвращает URLSession.dataTaskPublisher, который проверяет HTTP-ошибки (используя tryMap), decode ответ.

Теперь я должен убедиться, что Publisher автоматически " обновляет себя всякий раз, когда у последнего переданного значения был действительный URL-адрес next, и происходит событие завершения.

Я обнаружил, что существует метод Publisher.append, который точно сделает это, но у меня возникла проблема пока: я хочу добавить только при определенном условии (= valid next).

Следующий псевдокод иллюстрирует это:

func requestPublisher(for request: TvShowsSearchRequest, with url: URL) -> AnyPublisher<TvShowsSearchResponse, Error> {
    // ... build urlRequest, skipped here ...

    let apiCall = self.session.dataTaskPublisher(for: urlRequest)
        .tryMap { data, response -> Data in
            guard let httpResponse = response as? HTTPURLResponse else {
                throw APIError.server(message: "No HTTP response received")
            }
            if !(200...299).contains(httpResponse.statusCode) {
                throw APIError.server(message: "Server respondend with status: \(httpResponse.statusCode)")
            }
            return data
        }
        .decode(type: TvShowsSearchResponse.self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
    return apiCall
}


// Here I'm trying to use the Combine approach

var moreURL : String?

dataProvider.requestPublisher(request)
    .handleEvents(receiveOutput: {
        moreURL = $0.next   // remember the "next" to fetch more data
    })
    .append(dataProvider.requestPublisher(request, next: moreURL))  // this does not work, because moreUrl was not prepared at the time of creation!!
    .sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
    .store(in: &cancellableSet)

Я предполагаю, что есть люди там которые уже решили эту проблему реактивным путем. Всякий раз, когда я нахожу выполнимое решение, оно снова включает рекурсию. Я не думаю, что именно так должно выглядеть правильное решение.

Я ищу издателя, который отправляет ответы, а я не предоставляю функцию обратного вызова. Вероятно, должно быть решение с использованием Publisher of Publishers, но я еще не понимаю его.

После комментария @kishanvekariya я попытался собрать все с несколькими издателями:

  1. Издатель mainRequest, который получает ответ на "основной" запрос.

  2. Новый urlPublisher, который получает все next URL-адреса "основного" или любых последующих запросов.

  3. A новый moreRequest издатель, который извлекает для каждого значения urlPublisher новый запрос, отправляя все next URL-адреса обратно на urlPublisher.

Затем я попытался прикрепить moreRequest издатель на mainRequest с append.

var urlPublisher = PassthroughSubject<String, Error>()

var moreRequest = urlPublisher
    .flatMap {
        return dataProvider.requestPublisher(request, next: $0)
            .handleEvents(receiveOutput: {
                if let moreURL = $0.next {
                    urlPublisher.send(moreURL)
                }
            })
    }

var mainRequest = dataProvider.requestPublisher(request)
    .handleEvents(receiveOutput: {
        if let moreURL = $0.next {
            urlPublisher.send(moreURL)
        }
    })
    .append(moreRequest)
    .sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
    .store(in: &cancellableSet)

Но это все равно не работает ... Я всегда получаю результат "основного" запроса. Все последующие запросы отсутствуют.

1 Ответ

1 голос
/ 19 января 2020

Кажется, что я сам нашел решение.

Идея в том, что у меня есть urlPublisher, который инициализируется первым URL-адресом, который затем используется издателем mainRequest для подачи next URL-адрес для этого и последующий запрос на последующий запрос.

let url = endpoint(for: request)     // initial URL
let urlPublisher = CurrentValueSubject<URL, Error>(url)

let mainRequest = urlPublisher
    .flatMap {
        return dataProvider.requestPublisher(for: request, with: $0)
            .handleEvents(receiveOutput: {
                if let next = $0.next, let moreURL = URL(string: self.transformNextUrl(nextUrl: next)) {
                    urlPublisher.send(moreURL)
                }
            })
    }
    .sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
    .store(in: &cancellableSet)

Итак, в итоге я использовал композицию из двух издателей и flatMap вместо нефункциональных append. Вероятно, это тоже решение, на которое можно нацелиться с самого начала ...

...