如何使用 Swift 泛型处理成功和错误 API 响应?

Posted

技术标签:

【中文标题】如何使用 Swift 泛型处理成功和错误 API 响应?【英文标题】:How to handle success and error API responses with Swift Generics? 【发布时间】:2021-11-03 00:28:35 【问题描述】:

我正在尝试编写一个简单的函数来处理返回 JWT 令牌的身份验证 POST 请求。

我的LoopBack 4 API 将令牌作为 JSON 数据包返回,格式如下:

 "token": "my.jwt.token" 

如果发生错误,则返回以下内容:


  "error": 
    "statusCode": 401,
    "name": "UnauthorizedError",
    "message": "Invalid email or password."
  

如您所见,这些类型完全不同,它们没有任何共同的属性。

我定义了以下 Swift 结构来表示它们:

// Success
struct Token: Decodable 
  let token: String


// Error
struct TokenError: Decodable 
  let error: ApiError


struct ApiError: Decodable 
  let statusCode: Int
  let name: String
  let message: String

返回 Swift 泛型的身份验证请求的签名:

@available(ios 15.0.0, *)
func requestToken<T: Decodable>(_ user: String, _ password: String) async throws -> T

我一直在尝试对这个函数进行单元测试,但 Swift 要求我预先声明结果的类型:

let result: Token = try await requestToken(login, password)

这对于快乐路径非常有效,但如果身份验证不成功,则会引发 The data couldn’t be read because it is missing. 错误。我当然可以抓住它,但我无法将结果转换为我的 TokenError 类型以访问其属性。

我在 *** 上遇到了一些线程,一般建议是通过通用协议表示成功和错误类型,但由于与 Decodable 协议的冲突,我没有运气。响应类型已经符合。

所以问题是我的requestToken 函数返回的result 变量是否可以同时使用成功和错误。

【问题讨论】:

【参考方案1】:

最自然的方式,IMO 是抛出 ApiErrors,这样就可以像处理其他错误一样处理它们。看起来像这样:

将 ApiError 标记为错误类型:

extension ApiError: Error 

现在你可以直接解码 Token,如果 API 错误会抛出 ApiError,如果数据损坏会抛出 DecodingError。 (注意在第一次解码中使用try?,在else解码中使用try。这样如果数据根本无法解码就会抛出。)

extension Token: Decodable 
    enum CodingKeys: CodingKey 
        case token
    
    init(from decoder: Decoder) throws 
        if let container = try? decoder.container(keyedBy: CodingKeys.self),
           let token = try? container.decode(String.self, forKey: .token)
        
            self.init(token: token)
         else 
            throw try TokenError(from: decoder).error
        
    


// Usage if you want to handle ApiErrors specially

do 
    try JSONDecoder().decode(Token.self, from: data)
 catch let error as ApiError 
    // Handle ApiErrors
 catch let error 
    // Handle other errors


另一种方法是将 ApiErrors 与其他错误分开,在这种情况下,requestToken 可以通过三种可能的方式返回。它可以返回一个 Token,也可以返回一个 TokenError,也可以抛出一个解析错误。抛出错误由throws 处理。 Token/TokenError 需要一个“或”类型,它是一个枚举。这可以通过Result 完成,但这可能有点令人困惑,因为例程也会抛出。相反,我会很明确。

enum TokenRequestResult 
    case token(Token)
    case error(ApiError)

现在您可以通过首先尝试将其解码为 Token 来使其成为可解码,如果失败,请尝试将其解码为 TokenError 并从中提取 ApiError:

extension TokenRequestResult: Decodable 
    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()
        if let token = try?
            container.decode(Token.self) 
            self = .token(token)
         else 
            self = .error(try container.decode(TokenError.self).error)
        
    

要使用这个,你只需要切换:

let result = try JSONDecoder().decode(TokenRequestResult.self, from: token)

switch result 
case .token(let token): // use token
case .error(let error): // use error

【讨论】:

非常感谢您快速详细的回复,Rob。我真的很喜欢你的第一个建议,因为我认为它比另一个更优雅。尽管如此,我还是没有运气,因为代码在Token 扩展中的if 语句上中断(它抛出EXC_BAD_ACCESS)。我在那条线上打了一个断点,当我尝试跨过它时,它在一个网络线程下创建了另一个“SingleValueDecodingContainer.decode 的调度重击”条目,并且一遍又一遍地发生。我正在测试有效和无效请求,这两种情况都会发生。 很抱歉。我创建了一个我应该注意到的无限循环。我测试这个太快了。代码已更新。

以上是关于如何使用 Swift 泛型处理成功和错误 API 响应?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 swift 将数据发送到服务器(api)

子类化 NSObject 和使用泛型时 Swift 编译错误

为泛型 Swift 扩展一个类模型

如何在 swift 4 中处理 HTTP 加载失败(错误代码:-1009 [1:50])?

如何使用原生崩溃报告在 Swift 4 中实现异常/错误处理?

如何在 API 调用中设置完成处理程序 - Swift 5