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

How to handle success and error API responses with Swift Generics?

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

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

{ "token": "my.jwt.token" }

如果出现错误,将改为 return 编辑以下内容:

{
  "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
}

认证请求的签名 returns Swift Generics:

@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)

这对于 happy path 来说工作得很好,但如果身份验证不成功,则会抛出 The data couldn’t be read because it is missing. 错误。我当然可以抓住它,但我无法将结果转换为我的 TokenError 类型以访问其属性。

我在 Whosebug 上遇到过一些线程,其中一般建议是通过通用协议来表示成功和错误类型,但由于与 Decodable 响应类型已经符合的协议。

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

最自然的方式,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 可以 return 三种可能的方式。它可以return一个Token,也可以return一个TokenError,也可以抛出解析错误。抛出错误由 throws 处理。 Token/TokenError 需要一个“或”类型,它是一个枚举。这可以用 Result 来完成,但这可能有点混乱,因为例程也会抛出。相反,我会很明确。

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

现在您可以通过首先尝试将其解码为 Token 使其成为 Decodable,如果失败,请尝试将其解码为 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
}