Как успешно сопоставить сопоставления сбоев между двумя издателями (Never и URLError) - PullRequest
1 голос
/ 04 мая 2020

Изучив несколько различных ресурсов по Combine, в том числе книги Джозефа Хека и Донни Уолса, я близок к пониманию цепочки DataTaskPublishers, но не смог соединить их вместе в одну серию связанных операторов. Кажется, я зациклен на том факте, что ошибки между выходными данными первого издателя не соответствуют ожидаемым входным данным второго. Оба расширения Publisher работают, когда они не подключены, поэтому я уверен, что нет возможности объединить их. Я бы подумал, что mapError () сработал бы, но он не хочет компилироваться.

Вот настройка:

Имеется два пользовательских издателя:

extension Publisher where Output == MKCoordinateRegion, Failure == URLError {

func toRegionDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, URLError> {
    return self
        .flatMap({ region -> URLSession.DataTaskPublisher in
                  ...
                  ... 
                  ...
                  return URLSession.shared.dataTaskPublisher(for: request)       
                  })
        .eraseToAnyPublisher()
    }
}

и

extension Publisher where Output == [String], Failure == Never {

func toGeographiesDataTask() ->  AnyPublisher<URLSession.DataTaskPublisher.Output, URLError {
    return self
        .setFailureType(to: URLError.self)
        .flatMap({ ids -> URLSession.DataTaskPublisher in
                   ...
                   ...
                   ...
                  return URLSession.shared.dataTaskPublisher(for: request)
                 })
         .eraseToAnyPublisher()
}

}

У меня есть функция, которая пытается связать их вместе следующим образом:

   let passthroughSubj = PassthroughSubject<MKCoordinateRegion,URLError>()

    passthroughSubj
    .toRegionDataTask()                                         // returns <DataTaskPublisher, URLError>
    .map { $0.data }                                            // returns <FlatMap, ?>
    .decode(type: ApiResponse.self, decoder:JSONDecoder())      // returns <ApiResonse, ?>
    .map {$0.body.data(using: .utf8)! }                         // returns <Data, ?>
    .decode(type: AmznResponse.self, decoder: JSONDecoder())    // returns <AmznResponse, ?>
    .map ({ response -> [AmznItem] in                           //
                return response.contents                        // returns <[AmznItem], ?>
    })
    .map ({ items -> [String] in                                // returns <[String], Never> ?
            var ids = [String]()
            for item in items {
                    ids.append(item.geoid)
            }
            return ids
            })
//
//        .toGeographiesDataTask()                                  // get error "Referencing instance method
//        .map { $0.data }                                          // 'toGeographiesDataTask()' on 'Publisher'
//        .decode(type: ApiResponse.self, decoder:JSONDecoder())    // requires the types 'Error' and 'Never'
//        .map {$0.body.data(using: .utf8)! }                       // be equivalent"
//        .decode(type: AmznResponse.self, decoder: JSONDecoder())
//        .map { $0.contents }
//
    .sink(receiveCompletion: { (completion) in
        switch completion {
        case .failure(let error):
            print(error)
        case .finished:
            print("DONE")
        }
        }, receiveValue: { data in
           print(data)
        })
    .store(in: &cancellables)

passthroughSubj.send(region1)

Если я раскомментирую второго пользовательского издателя, я получу сообщение об ошибке показано справа. Насколько я понимаю, .map возвращал <[String], Never>, но в итоге из-за сбоя DataTaskPublisher мне нужно вместо этого сопоставить его с URLError. Но ни одна комбинация .mapError, похоже, тоже не компилируется.

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

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

Любая помощь или указатели будут очень рады! Спасибо.

Ответы [ 2 ]

0 голосов
/ 05 мая 2020

Прежде всего, у вас есть несколько декодируемых типов, которые нам нужно смоделировать, чтобы поиграться:

struct ApiResponse: Decodable {
    var body: String
}

// Does the abbreviation "Amzn" really improve the program?
struct AmazonResponse: Decodable {
    var contents: [AmazonItem]
}

struct AmazonItem: Decodable {
    var geoid: String
}

Затем у вас есть пара пользовательских Publisher операторов, каждый из которых должен создать URLRequest. Давайте уменьшим вложение и позволим Swift выводить больше типов, выделив этот код:

func apiRequest(for region: MKCoordinateRegion) -> URLRequest {
    // Your code here. fatalError gets this through the compiler.
    fatalError()
}

func geographiesRequest(forIds ids: [String]) -> URLRequest {
    // Your code here. fatalError gets this through the compiler.
    fatalError()
}

Теперь давайте рассмотрим ваш первый пользовательский оператор, toRegionDataTask.

  • You определили его только для издателей, где Failure == URLError. Может быть, это то, что вы действительно хотите, но, так как мы все равно будем декодировать нисходящий поток, а декодирование имеет тип Error, то *1016*, давайте просто будем использовать Error.

  • Вы должны были вручную указать тип Publisher, возвращаемый преобразованием flatMap. Поскольку мы выделили apiRequest(for:), нам больше не нужно это делать.

Поэтому мы можем попробовать это:

extension Publisher where Output == MKCoordinateRegion {
    func toRegionDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
        return self
            .map { apiRequest(for: $0) }
            .flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
            .eraseToAnyPublisher()
    }
}

Но горе нам, Компилятор имеет жалобы:

ошибка: без названия Page.xcplaygroundpage: 31: 18: ошибка: метод экземпляра 'flatMap (maxPublishers: _ :)' требует типы 'Self.Failure' и 'URLSession.DataTaskPublisher .Failure '(он же' URLError ') должен быть эквивалентен

            .flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
             ^

error: без названия Page.xcplaygroundpage: 32: 18: error: невозможно преобразовать возвращаемое выражение типа' AnyPublisher '(aka' AnyPublisher <(data: Данные, ответ: URLResponse), Self.Failure> ') для возврата типа «AnyPublisher» (он же «AnyPublisher <(данные: Данные, ответ: URLResponse), Ошибка>»)

            .eraseToAnyPublisher()
             ^

Страница без названия. xcplaygroundpage: 32: 18: примечание: ожидается, что аргументы универсального параметра 'Failure' ('Self.Failure' и 'Error') будут равны

            .eraseToAnyPublisher()
             ^

Способ отладки это разбейте его на несколько шагов и используйте eraseToAnyPublisher после каждого шага, чтобы увидеть Output и Failure ty pes:

    func toRegionDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
        let x = self
            .map { apiRequest(for: $0) }
            .eraseToAnyPublisher()

        let y = x
            .flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
            .eraseToAnyPublisher()

        return y
    }

Теперь мы можем видеть (щелкнув по опции x), что после map, Output равно URLRequest и Failure равно Self.Failure - независимо от типа ошибки self производит. Это имеет смысл, потому что я удалил ограничение Failure == URLError из расширения.

Теперь компилятор просто выдает первую из предыдущих жалоб:

Ошибка: Untitled Page.xcplaygroundpage: 34 : 18: ошибка: метод экземпляра «flatMap (maxPublishers: _ :)» требует, чтобы типы «Self.Failure» и «URLSession.DataTaskPublisher.Failure» (также известный как «URLError») были эквивалентны

Это говорит, что тип «input» Failure для оператора flatMap должен совпадать с типом «output» Failure. Тип ввода Self.Failure, а вывод URLError. Вероятно, поэтому вы ограничили Failure == URLError расширением. Но я предпочитаю решать это по-разному, конвертируя оба типа ошибок в Error, используя mapError. Это облегчает написание тестов для метода и изменение его использования в будущем. Вот что я хотел бы сделать:

extension Publisher where Output == MKCoordinateRegion {
    func toRegionDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
        let x = self
            .map { apiRequest(for: $0) }
            .mapError { $0 as Error }
         // ^^^^^^^^^^^^^^^^^^^^^^^^^
            .eraseToAnyPublisher()

        let y = x
            .flatMap { URLSession.shared.dataTaskPublisher(for: $0).mapError { $0 as Error } }
                                                                 // ^^^^^^^^^^^^^^^^^^^^^^^^
            .eraseToAnyPublisher()

        return y
    }
}

Наконец, мы можем удалить промежуточные шаги, чтобы получить окончательную версию:

extension Publisher where Output == MKCoordinateRegion {
    func toRegionDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
        return self
            .map { apiRequest(for: $0) }
            .mapError { $0 as Error }
            .flatMap { URLSession.shared.dataTaskPublisher(for: $0).mapError { $0 as Error } }
            .eraseToAnyPublisher()
    }
}

Мы дадим toGeographiesDataTask такую ​​же обработку:

extension Publisher where Output == [String] {
    func toGeographiesDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
        return self
            .map { geographiesRequest(forIds: $0) }
            .mapError { $0 as Error }
            .flatMap { URLSession.shared.dataTaskPublisher(for: $0).mapError { $0 as Error } }
            .eraseToAnyPublisher()
    }
}

Вы можете заметить, что toRegionDataTask и toGeographiesDataTask теперь почти идентичны. Но я собираюсь оставить это в покое для этого ответа.

Так или иначе, теперь давайте посмотрим на ваш длинный конвейер. Вы получили сообщение об ошибке, потому что ваш toGeographiesDataTask имел ограничение Failure == Never, но предшествующий ему оператор map не имеет Failure тип Never. Он имеет тот же тип Failure, что и восходящий, то есть Error (из-за оператора decode(type:decoder:)).

Поскольку я удалил это ограничение из toGeographiesDataTask, конвейер больше не имеет этой ошибки , Мы можем немного очистить извлечение geoid:

// Does the abbeviation "subj" really improve the program?
// The subject's Failure type could be anything here.
let subject = PassthroughSubject<MKCoordinateRegion, Error>()

var tickets: [AnyCancellable] = []

subject
    .toRegionDataTask()
    .map { $0.data }
    .decode(type: ApiResponse.self, decoder: JSONDecoder())
    .map { $0.body.data(using: .utf8)! }
    .decode(type: AmazonResponse.self, decoder: JSONDecoder())
    .map { $0.contents }
    .map { $0.map { $0.geoid } }
    .toGeographiesDataTask()
    .map { $0.data }
    .decode(type: ApiResponse.self, decoder: JSONDecoder())
    .map { $0.body.data(using: .utf8)! }
    .decode(type: AmazonResponse.self, decoder: JSONDecoder())
    .map { $0.contents }
    .sink(
        receiveCompletion: { print("completion: \($0)") },
        receiveValue: { print("value: \($0)") })
    .store(in: &tickets)

let region1 = MKCoordinateRegion()
subject.send(region1)
0 голосов
/ 04 мая 2020

Оператор map преобразует только Output, оставляя Error без изменений. Поэтому, если бы я заполнил пробелы в ваших парах Output и Failure, я бы в итоге получил следующее:

// returns <DataTaskPublisher, URLError>
// returns <Data, URLError>
// returns <ApiResonse, Error> (decode replaces the Failure with Error)
// returns <Data, Error>
// returns <AmznResponse, Error>
// returns <[AmznItem], Error>
// returns <[String], Error>

Ваша реализация toGeographiesDataTask требует, чтобы издатель, к которому она применяется, имел Never как ошибка, поэтому вы получаете ошибку компилятора, которую вы получаете.

Я думаю, вы можете удалить требование ошибки из вашего расширения и сделать его

extension Publisher where Output == [String] {
  // implementation
}

Тогда внутри toGeographiesDataTask() вы можете заменить URLError, который выдается задачей данных, используя mapError:

func toGeographiesDataTask() ->  AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
    return self
        .flatMap({ ids -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> in
                   ...
                   ...
                   ...
                  return URLSession.shared.dataTaskPublisher(for: request)
                    .mapError({ $0 as Error})
                    .eraseToAnyPublisher()
                 })
         .eraseToAnyPublisher()
}

Я думаю, что это должно заставить остальную часть цепочки работать тоже, и вы должны в итоге <[AmznItem], Error> как <Output, Failure> в конце цепочки.

Я не пробовал это на игровой площадке, но уверен, что это должно помочь вам.

...