Build Status Platforms Carthage Compatible Swift Package Manager Compatible Xcode version

Lift 是一个 Swift 库,用于生成值并将值提取到类似 JSON 的数据结构中。 Lift 的设计充分考虑了以下要求

用法示例

Lift 简单而强大。 让我们看看如何将其与自定义类型一起使用

struct User {
  let name: String
  let age: Int
}

只需遵守 JarElement 协议,即可让 Lift 知道如何转换您的类型

extension User: JarElement {
  init(jar: Jar) throws {
    name = try jar["name"]^
    age = try jar["age"]^
  }
 
  var jar: Jar {
    return ["name": name, "age": age]
  }
}

然后,给定一些 JSON,您现在可以构造一个 Jar 并使用 lift 运算符 ^ 从中提取用户

let json = "[{\"name\": \"Adam\", \"age\": 25}, {\"name\": \"Eve\", \"age\": 20}]"
let jar = try Jar(json: json)
var users: [User] = try jar^

将您的模型值移回 JSON 也同样容易

users.append(User(name: "Junior", age: 2))
let newJson = try String(json: Jar(users), prettyPrinted: true)

Lift 甚至可以与其他类似 JSON 的结构化数据一起使用,例如 p-list 和 UserDefaults

let users: [User] = try UserDefaults.standard["users"]^

查看用法部分,了解更多信息和示例。

目录

要求

安装

Carthage

github "iZettle/Lift" >= 2.3

Cocoa Pods

platform :ios, '9.0'
use_frameworks!

target 'Your App Target' do
  pod 'Lift', '~> 2.3'
end

Swift Package Manager

import PackageDescription

let package = Package(
  name: "Your Package Name",
  dependencies: [
      .Package(url: "https://github.com/iZettle/Lift.git",
               majorVersion: 2)
  ]
)

关于 Codable 的说明

Swift 4 引入了 Codable,并“承诺”一劳永逸地解决与 JSON 的交互。 是的,显示的许多示例都非常神奇。 但是,当您的模型开始从简单模型转变为模型和 JSON 之间的映射时,这种魔力似乎就消失了。 现在您又回到自己实现一切,并且使用的是相当冗长的 API。 当前版本的 Swift 还缺少用于动态构建和解析 JSON 的 API(不经过模型对象),这在例如构建和解析网络请求时很常见。 因此,我们相信第三方 JSON 库的需求在未来一段时间内仍然存在。

用法

简介

让我们从一个简单的示例开始,了解如何从一些键值结构化数据(例如 JSON)中提取数据

let jar: Jar = ["name": "Lift", "version": 1.0]
let name: String = try jar["name"]^
let version: Double = try jar["version"]^

Jar 是 Lift 的异构值容器。 在此示例中,它保存一个字典。 运算符 ^(称为 lift 运算符)用于从 jar 容器中提取值。 因为 jar 通常保存编译时未知的值,所以提取它们可能会失败。 如果值缺失、值类型与预期不符或某些其他验证失败,则可能会发生这种情况。 这就是为什么您总是会在 lift 运算符 ^ 存在时看到 try

如前所述,JSON 并不总是以字典(键值)的形式出现,也可以是简单的原始类型或其他 JSON 对象的数组

let i: Int = try Jar(1)^
let b: Bool = try Jar(true)^
let a: [Int] = try Jar([1, 2, 3])^
let jar: Jar = ["value": "lift"]
let s: String = try jar["value"]^

^ 运算符被重载,以允许一致的类型提取为类型本身或其可选版本。 您还可以提取一致类型的数组或可选数组

let i: Int = try jar^
let i: Int? = try jar^
let i: [Int] = try jar^
let i: [Int]? = try jar^

Jar 实现了键和索引的下标操作,并允许它们嵌套

let date: Date = try jar["payments"][3]["date"]^
jar["payments"][2]["date"] = Date()

JSON 序列化

Lift 添加了便捷的初始化程序,用于从 JSON 构造 Jar 和将其返回

let json = "{ \"val\": 3, \"vals\": [ 1, 2 ] }"
let jar = try Jar(json: json)
let jsonString = try String(json: jar, prettyPrinted: true)
let jsonData = try Data(json: jar, prettyPrinted: false)

您也可以自己处理序列化,只需传递一个 Any

let json: Any = ...
let jar = try Jar(checked: json) // Will validate when constructed - slower
let jar = Jar(unchecked: json) // Will lazily validate at access - faster

let any: Any = try jar.asAny()

生成 JSON

为了帮助创建 JSON,Jar 实现了几个按字面量可表达的协议,因此您可以编写如下代码

func send(_ jar: Jar) { ... }

send(true)
send(1)
send(3.14)
send("Hello")
send(["val": 5])
send([1, 2, 3])

Jar 无法被编译器推断时,您可以显式指定类型

let jar: Jar = ["val": 5]
let jar: Jar = [1, 2, 3]

您还可以构建嵌套的层次结构

let jar: Jar = [5, ["val": [1, 2]]]

当然,您也可以从自定义类型构建 JSON

let jar: Jar = ["payment": payment, "date": date]

修改 JSON

因为 Jar 是一种具有值语义的值类型,所以您可以修改声明为 varJar 值。

var jar = Jar()
jar["payment"] = payment
jar["date"] = Date()
var jar = Jar()
jar.append(payment)
jar.append(Date())

如果需要在传递 JSON 之前对其进行修改,只需复制一份

func receive(jar: Jar) {
  var jar = jar
  jar["timeReceived"] = Date()
  // ...
}

Jar 永远不会绑定到特定的类型或值,因此始终可以更改它

var jar: Jar = true // jar holds a boolean
jar = [1, 2] // holds an array
jar["val"] = 1 // holds a dictionary
jar = 4711 // holds an integer
jar = ["val2": 2] // holds a dictionary
jar = "Hello" // holds a string

数组

Lift 支持使用原始类型数组

var jar: Jar = [1, 2, 3]

jar[1] = 4
jar.append(5)

let val: Int = try jar[2]^

以及自定义类型的数组

var jar: Jar = [Payment(...), Payment(...), ...]

let payments: [Payment] = try jar^
jar[2] = Payment(...)

缺失值和 JSON null

有时需要存在值,有时它是可选的。

let i: Int = try jar["val"]^ // Will throw if val is missing or not an Int
let i: Int? = try jar["val"]^ // Will return nil if val is missing or null, else throw if not an Int
let i: Int = try jar["val"]^ ?? 4711 // Will throw if val is present and is not an Int

为了您的方便,Lift 将设置为 JSON null 的值视为与缺失值相同。 但是,如果您需要检查实际 null 值的存在,您可以编写

if let _: Null = try? jar["val"]^ {
  //...
}

在构建 JSON 时,某些值是可选的非常常见

let optional: Int? = nil
var jar: Jar = ["val": 1]
jar["optional"] = optional // -> {"val": 1}

也可以内联添加可选值

var jar: Jar = ["val": 1, "optional": optional] // -> {"val": 1}

如果您实际想要一个 JSON null 值,可以使用常量 null

var jar: Jar = ["val": 1, "optional": optional ?? null] // -> {"val": 1, "optional": null}

异构值

有时您的 JSON 的某些部分可能包含不同类型的有效类型的联合。 然后,您可以测试您支持的不同变体

let any: Any? = NSJSONSerialization...
let jar = try Jar(any) // any could be a dictionary, array or a primitive type

if let int: Int = try? jar^ {
  // ...
} else if let ints: [Int] = try? jar^ {
  // ...
} else if let int: Int = try? jar["value"]^ {
  // ...
}

JSON 也支持混合类型的数组

let jar = try Jar(any) // [ 1, [1, 2], { "val" : 3 } ] -- [Int, Array, Dictionary]

let int: Int = try jar[0]^
let array: [Int] = try jar[1]^
let dict: Jar = try jar[2]^

值的转换

有时您需要在从 Jar 提取值之前对其进行转换。 当您使用无法遵守 JarRepresentable 的类型时,可能会发生这种情况,例如在使用元组时

typealias User = (name: String, age: Int)
let users: [User] = try (jar^ as [Jar]).map { jar in 
  try (jar["name"]^, jar["age"]^) 
}

或者当您的类型不符合 JarRepresentable 时,因为它可能需要一些额外的初始化数据

let account: Account = ...

let payments: [Payment] = try (jar["payments"]^ as Array).map {
  Payment(jar: $0, account: account)
}

即使您可以手动转换值以添加额外的初始化数据,但通常更方便的是将此数据添加到 jar 的上下文中。 jar 上下文将在下面进一步描述。

超越 JSON

设置和获取未知类型的值并非 JSON 独有。 许多 Cocoa API 使用字典,其中许多基于与 JSON 类似的原则,例如 p-list。 Lift 提供了用于扩展这些类型的协议,以授予它们访问 Lift 功能的权限。 例如,Lift 已经扩展了 UserDefaults

// extension UserDefaults: MutatingValueForKey { }

let userDefaults = UserDefaults.standard

let date: Date? = try userDefaults["lastLaunched"]^
userDefaults["lastLaunched"] = Date()

let payments: [Payments] = try userDefaults["payments"]^ ?? []

JarConvertible & JarRepresentable

开箱即用,Lift 库支持 JSON 字典、数组及其原始类型:字符串、数字、布尔值和 null。 但是,也很容易扩展您自己的类型以与 Lift 库一起使用。

为了能够使用 lift 运算符 ^Jar 中提取值,您需要使您的类型符合 JarConvertible 协议

protocol JarConvertible {
  init(jar: Jar) throws
}

为了能够将您的类型转换为 Jar,您需要使您的类型符合 JarRepresentable 协议

protocol JarRepresentable {
  var jar: Jar { get }
}

通常会同时实现这两个协议,因此可以使用方便的 JarElement 类型别名

typealias JarElement = JarConvertible & JarRepresentable

Lift 库包括最常见原始类型(例如 IntBoolString 等)的扩展,通过使它们符合 JarElement

处理自定义类型

您的自定义类型通常是简单类型,例如

struct Money {
  let fractionized: Int
}

extension Money: JarElement {
  init(jar: Jar) throws {
    fractionized = try jar^
  }

  var jar: Jar { return Jar(fractionized) }
}

let jar: Jar = ["amount": Money(fractionized: 2000)]
let amount: Money = try jar["amount"]^

或者更常见的是,更复杂和类似记录的类型,例如

struct Payment {
  let amount: Money
  let date: Date
}

extension Payment: JarElement {
  init(jar: Jar) throws {
    amount = try jar["amount"]^
    date = try jar["date"]^
  }

  var jar: Jar {
    return ["amount": amount, "date": date]
  }
}

let jar: Jar = ["payment": Payment(...)]
let payment: Payment = try jar["payment"]^

为了更轻松地使具有原始值的自定义枚举符合要求,Lift 提供了一些默认实现。 您所要做的就是使枚举符合 JarElement,以便能够将其与 Jar 一起使用

enum MyEnum: String, JarElement {
  case one, two, three
}

let jar: Jar = ["enum": MyEnum.two]
let str: String = try jar["enum"]^ // -> "two"
let myEnum: MyEnum = try jar["enum"]^ // -> .two

JarConvertible 要求您实现一个必需的 init。 如果您使用无法更新源本身的非最终类,例如当该类源自 Objective-C 或其他外部源时,这可能会出现问题。 在这些情况下,您必须改用 Liftable 协议

extension MyClass: Liftable {
  static func lift(from jar: Jar) throws -> MyClass {
    // Implementation
  }
}

模型结构

Lift 库不强制规定自定义类型的结构,也允许追溯建模。 如何决定在类型和 JSON 之间进行映射取决于您。 例如,您可能有带有关联值的枚举(在此示例中是递归的)

// [ { "type": "Product", "uuid": ”3b0bb980-2c…” },
//   { "type": "Folder", "name": "Coffee", "items": [
//        { "type": "Product", "uuid": ”3e493140-2c…” },
//        { "type": "Product", ”uuid": ”3e623780-2c…” }] },
//   ... ]

indirect enum FlowLayout {
  case product(uuid: UUID)
  case folder(name: String, items: [FlowLayout])
}

因为 JSON 格式的类型系统比 Swift 弱,所以更严格的验证变得更加重要

extension FlowLayout: JarConvertible  {
  init(jar: Jar) throws {
    switch try jar["type"]^ as String {
    case "Product":
      self = try .product(uuid: jar["uuid"]^)
    case "Folder":
      self = try .folder(name: jar["name"]^, items: jar["items"]^)
    case let type:
      throw jar.assertionFailure("Unknown layout type: \(type)")
    }
  }
}

即使对于这些更复杂的类型,JSON 的生成仍然非常简单

extension FlowLayout: JarRepresentable {
  var jar: Jar {
      switch self {
      case let .product(uuid):
        return ["type": "Product", "uuid": uuid]
      case let .folder(name, items):
        return ["type": "Folder", "name": name, "items": items]
    }
  }
}

处理错误

因为 JSON 通常是嵌套的,所以使用一些定位和上下文扩展错误会很有用。 Lift 尝试跟踪最接近的上下文和到您的数据的“键路径”,并将这些信息公开在 LiftError

struct LiftError: Error {
  let message: String
  let key: String
  let context: String
}

因为上下文和键路径在调试期间确实很有价值,所以重要的是在抛出验证错误时不要丢失这些信息。 因此,Lift 已向 Jar 添加了特殊的断言辅助方法,建议您使用

init(jar: Jar) throws {
  // ...
  try jar.assert(i > 0, "Must greater than zero")

  guard validate(...) else {
    throw jar.assertionFailure("Not a business nor a person")
  }
  
  url = try jar.assertNotNil(URL(string: jar^), "Invalid URL")
  // ...
}

Jar 上下文

有时您的类型的初始化程序需要访问比 JSON 本身包含的更多数据。 例如,您的 Money 类型可能还需要一个货币,但您的 JSON 不提供该货币,或者提供的货币距离实际金额值很远。 这是您可以将货币传递到 jar 的上下文中的位置

struct Money {
  let fractionized: Int
  let currency: Currency
}

extension Money: JarElement {
  init(jar: Jar) throws {
    fractionized = try jar^
    currency = try jar.context.get() // will extract the currency
  }

  var jar: Jar { return Jar(fractionized) }
}

let amount: Money = try jar.union(context: currency)["amount"]^

jar 的上下文也可用于自定义类型的编码和解码。 例如,Date 默认将使用 ISO8601 日期格式,但通过在 jar 的上下文中提供另一个 DateFormatter,您可以自定义日期格式

extension Date: JarConvertible, JarRepresentableWithContext {
  init(jar: Jar) throws {
    let formatter: DateFormatter = jar.context.get() ?? .iso8601
    self = try jar.assertNotNil(formatter.date(from: jar^), "Date failed to convert using formatter with dateFormat: \(formatter.dateFormat)")
  }

  func asJar(using context: Jar.Context) -> Jar {
    let formatter: DateFormatter = context.get() ?? .iso8601
    return Jar(formatter.string(from: self))
  }
}

由于 JarRepresentable 不提供任何上下文,因此您将改为遵守 JarRepresentableWithContext,后者在 asJar 中传递上下文

protocol JarRepresentableWithContext: JarRepresentable {
  func asJar(using context: Jar.Context) -> Jar
}

上下文可以从外部设置,也可以作为某些其他类型的编码/解码的一部分设置,例如

struct Payment {
  let amount: Money
  let date: Date
}

extension Payment: JarElement {
  init(jar: Jar) throws {
    let jar = jar.union(context: DateFormatter.custom)
    amount = try jar["amount"]^ // a currency must be provided in the jar's context
    date = try jar["date"]^ // date will format using DateFormatter.custom
  }

  var jar: Jar {
    let jar: Jar = ["amount": amount, "date": date]
    return jar.union(context: DateFormatter.custom)
  }
}

let payment: Payment = try jar.union(context: currency)["payment"]^

经过实地测试

Lift 是在过去几年中开发、演进和经过实地测试的,并且在 iZettle 广受好评的销售点应用程序中被广泛使用,用于与 iZettle 的全方位后端服务进行通信。

协作

您可以在我们的 Slack 工作区与我们协作。 您可以提问、分享想法,或者只是参与正在进行的讨论。 要获得邀请,请发送邮件至 ios-oss@izettle.com

了解更多

要了解更多关于 Lift 的 API 如何演变的信息,我们建议您阅读以下文章: