如何成功匹配两个发布服务器(从不和 URLError)之间的失败映射



在搜索了Combine上的一些不同资源之后,包括Joseph Heck和Donny Wals的书,我接近理解DataTaskPublishers的链式,但未能将它们连接成一系列链式运算符。 我似乎对第一个发布者的输出之间的错误与第二个发布者的预期输入不匹配这一事实感到困扰。 两个发布者扩展在未连接时都可以工作,因此我确信缺乏连接两者的能力。 我本以为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 将它们链接在一起的示例,但由于我正在将一个输出转换为第二个自定义发布者的输入,这似乎是不可能的。

任何帮助或指示将非常受欢迎! 谢谢。

首先,你需要模拟一些可解码的类型,以便玩:

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

  • 您仅为Failure == URLError.也许这就是你真正想要的,但由于我们无论如何都要在下游解码,并且解码有一种Failure类型的Error,让我们自始至终使用Error

  • 必须手动指定flatMap转换返回的Publisher类型。由于我们排除了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()
}
}

但是我们有祸了,因为编译器有抱怨:

错误:

无标题页面.xcplayground页面:31:18:错误:实例方法 'flatMap(maxPublishers:_:)' 需要 'Self.Failure' 和 'URLSession.DataTaskPublisher.Failure' (又名 'URLError') 是等效的

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

错误: 无标题页面.xcplaygroundpage:32:18: 错误: 无法转换 返回类型的表达式 "任何出版商"(又名 'AnyPublisher<(data: data, response: URLResponse), Self.Failure>') to 返回类型"任何发布者" (又名 'AnyPublisher<(data: data, response: URLResponse), Error>')

.eraseToAnyPublisher()
^

无题页面.xcplaygroundpage:32:18:注意:泛型参数 参数"失败"("自我失败"和"错误")应为 平等

.eraseToAnyPublisher()
^

调试它的方法是将其分解为多个步骤,并在每个步骤后使用eraseToAnyPublisher来查看OutputFailure类型:

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之后,OutputURLRequestFailureSelf.Failure- 无论self产生什么故障类型。这是有道理的,因为我从扩展中删除了约束Failure == URLError

编译器现在只发出前面的第一个抱怨:

错误:无标题页面.xcplaygroundpage:34:18:错误:实例方法"flatMap(maxPublishers:_:)"要求类型"Self.Failure"和"URLSession.DataTaskPublisher.Failure"(又名"URLError")等效

这表示flatMap运算符的"输入"Failure类型必须与"输出"Failure类型相同。输入类型为Self.Failure,输出为URLError。这可能就是您在扩展上限制Failure == URLError的原因。但我更喜欢使用mapError将两种故障类型转换为Error来解决它。这样可以更轻松地为该方法编写测试,并更改将来的使用方式。以下是我会做的:

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()
}
}

您可能会注意到,toRegionDataTasktoGeographiesDataTask现在几乎相同。但是对于这个答案,我将不考虑这个问题。

所以无论如何,现在让我们看看你的长管道。您收到错误是因为您的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)

map运算符只变换Output,它使Error保持不变。因此,如果我填写您的OutputFailure对上的空白,我最终会得到这个:

// 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()内部,您可以使用mapError替换数据任务发出的URLError

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>

我还没有在操场上尝试过这个,但我相当确定这应该让你继续前进。

最新更新