弹性解码

Swift Package Manager compatible Version License Platform

简介

此软件包定义了在解码 Decodable 类型时,从错误中部分恢复的机制。它还旨在提供符合人体工程学的 API,以便在开发期间检查解码错误,并在生产环境中报告它们。

更多细节如下,但这里简要介绍一下此软件包的功能

struct Foo: Decodable {
  @Resilient var array: [Int]
  @Resilient var value: Int?
}
let foo = try JSONDecoder().decode(Foo.self, from: """
  {
    "array": [1, "2", 3],
    "value": "invalid",
  }
  """.data(using: .utf8)!)

运行此代码后,foo 将是一个 Foo 类型,其中 foo.array == [1, 3]foo.value == nil。在 DEBUG 模式下,foo.$array.results 将会是 [.success(1), .failure(DecodingError.dataCorrupted(…), .success(3)],并且 foo.$value.error 将会是 DecodingError.dataCorrupted(…)。此功能仅在 DEBUG 模式下可用,以便我们可以保持 在发布版本中没有额外开销

设置

Swift Package Manager

在你的 Package.swift 文件中

  dependencies: [
    .package(name: "ResilientDecoding", url: "https://github.com/airbnb/ResilientDecoding.git", from: "1.0.0"),
  ]

CocoaPods

在你的 Podfile 文件中

platform :ios, '12.0'
pod 'ResilientDecoding', '~> 1.0'

解码

此软件包的主要接口是 @Resilient 属性包装器。它可以应用于四种类型的属性:OptionalArrayDictionary 和符合此软件包提供的 ResilientRawRepresentable 协议的自定义类型。

Optional

Optionals 是可以被赋予 Resilient 特性的最简单类型的属性。 声明为 @Resilient var foo: Int? 的属性将被初始化为 nil,如果在解码期间遇到错误(例如,如果 foo 键的值是 String),则不会抛出错误。

Array

Resilient 也可以应用于数组或可选数组 ([T]?)。 声明为 @Resilient var foo: [Int] 的属性,如果 foo 键缺失,或者该值是意外的值(例如 String),则将初始化为空数组。 同样,如果此数组的任何元素解码失败,则该元素将被省略。 此可选数组变体将在键丢失或具有空值时将值设置为 nil,否则设置为一个空数组。

Dictionary

Resilient 也可以应用于(字符串键控的)字典或可选字典 ([String: T]?)。 声明为 @Resilient var foo: [String: Int] 的属性,如果 foo 键缺失,或者该值是意外的值(例如 String),则将初始化为空字典。 同样,如果字典中的任何解码失败,则该值将被省略。 此可选字典变体将在键丢失或具有空值时将值设置为 nil,否则设置为一个空字典。

ResilientRawRepresentable

自定义类型可以遵循 ResilientRawRepresentable 协议,该协议允许它们在作为 Resilient 属性解码时自定义其行为(否则没有影响)。 ResilientRawRepresentable 继承自 RawRepresentable,主要用于原始值的 enum 遵循。 ResilientRawRepresentable 有两个静态属性:decodingFallbackisFrozen

decodingFallback

ResilientRawRepresentable 类型可以选择定义 decodingFallback,这使其可以在不包装在 optional 中的情况下进行弹性解码。 例如,以下枚举可以在编写为 @Resilient var myEnum: MyEnum 的属性中使用

enum MyEnum: String, ResilientRawRepresentable {
  case existing
  case unknown
  static var decodingFallback: Self { .unknown }
}

注意: ResilientRawRepresentableArrayDictionary 总是省略元素,而不是使用 decodingFallback

isFrozen

isFrozen 控制新的 RawValues 是否会将错误报告给 ResilientDecodingErrorReporter。 默认情况下,isFrozenfalse,这意味着 init(rawValue:) 返回 nilRawValue 不会报告错误。 当你希望旧版本的代码支持新的 enum 情况而不报告错误时,这非常有用,例如,当演进 iOS 应用程序使用的后端 API 时。 通过这种方式,该属性类似于 Swift 的 @frozen 属性,尽管它们实现了不同的目标。 isFrozen 对属性级别的错误没有影响。

检查错误

Resilient 提供了两种检查错误的机制,一种设计用于开发期间,另一种设计用于报告生产环境中意外的错误。

属性级别错误

DEBUG 版本中,Resilient 属性提供了一个 projectedValue,其中包含有关解码期间遇到的错误的信息。可以使用 $property.outcome 属性检查此信息,这是一个枚举,其 cases 包括 keyNotFoundvalueWasNil。这与错误不同,因为上述两种情况在属性值为 Optional 时实际上并不是错误。标量类型(例如 OptionalResilientRawRepresentable)还提供了一个 error 属性。开发人员可以通过访问编写为 @Resilient var foo: Int? 的属性的 $foo.error 来确定解码期间是否发生错误。 @Resilient 数组属性提供了两个附加字段:errorsresultserrors 是解码数组时恢复的所有错误的列表。 results 将这些错误与已成功解码的数组元素交错在一起。 例如,在解码 JSON 代码段 [1, 2, "3"] 时,编写为 @Resilient var baz: [Int] 的属性的 results 将是两个 .success 值,后跟一个 .failure

ResilientDecodingErrorReporter

在生产中,ResilientDecodingErrorReporter 可用于整理使用 Resilient 属性解码类型时遇到的所有错误。 JSONDecoder 提供了一个方便的 decode(_:from:reportResilientDecodingErrors:) API,如果遇到错误,该 API 返回解码后的值和错误摘要。 更复杂的用例需要在你的 DecoderuserInfo 中添加一个 ResilientDecodingErrorReporter,作为 .resilientDecodingErrorReporter 用户信息键的值。 解码类型后,你可以调用 flushReportedErrors,如果遇到任何错误,它将返回一个 ErrorDigest。 该摘要可用于访问底层错误 (errorDigest.errors) 或在 DEBUG 中进行漂亮的打印 (debugPrint(errorDigest))。

漂亮打印的摘要看起来像这样

resilientArrayProperty
  Index 1
    - Could not decode as `Int`
  Index 3
    - Could not decode as `Int`
resilientRawRepresentableProperty
  - Unknown novel value "novel" (this error is not reported by default)

注意:属性包装器上可用的错误与报告给 ResilientDecodingErrorReporter 的错误之间的一个区别是,后者默认情况下不报告 UnknownNovelValueErrors(当非冻结的 ResilientRawRepresentableinit(rawValue:) 返回 nil 时,会抛出 UnknownNovelValueError)。 你可以通过在错误摘要上调用 errors(includeUnknownNovelValueErrors: true) 来更改此行为。

常见问题

当包装的类型是泛型参数时,Resilient 是否会按预期工作?

否。 如果你的类型是关于 <T> 的泛型,并且指定 @Resilient var someResilient: T,那么无论 T 是数组还是字典,都将被视为单个值。

为什么 Resilient 在其值符合 Encodable 时不符合 Encodable 协议?

我们没有明确地让 Resilient 符合 Encodable,因为在存在错误的情况下,编码可能会有损。 如果你确定这对于你的用例不是问题,那么在你自己的模块中提供一个 Encodable 一致性应该很简单。

更多详情

有关特定 Resilient 字段在遇到特定错误时究竟如何表现的更多信息,我建议查阅单元测试。