【问题标题】:How to handle success and error API responses with Swift Generics?如何使用 Swift 泛型处理成功和错误 API 响应?
【发布时间】: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 类型以访问其属性。

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

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

【问题讨论】:

    标签: swift generics async-await decodable loopback4


    【解决方案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 的调度重击”条目,并且一遍又一遍地发生。我正在测试有效和无效请求,这两种情况都会发生。
    • 很抱歉。我创建了一个我应该注意到的无限循环。我测试这个太快了。代码已更新。
    猜你喜欢
    • 2018-09-20
    • 2015-10-10
    • 2020-09-10
    • 2017-07-28
    • 2019-01-07
    • 2018-06-20
    • 1970-01-01
    • 2020-06-11
    • 2019-04-02
    相关资源
    最近更新 更多