Lift是一个用于对JSON-like数据结构进行生成和提取值的Swift库。Lift经过精心设计,以满足以下要求
- 使用子脚本进行简单直观的语法。
- 可扩展,以用于您的自定义类型。
- 支持回溯建模/遵守规范。
- 不强制规定如何结构化您的数据模型。
- 类型安全并清楚地报告错误。
- 与任何键值结构化数据(如清单和用户默认值)一起使用。
- 提供详细的错误信息并支持自定义验证。
- 为
Jar
容器使用值语义。
示例用法
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
并从中提取用户
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-like结构化数据(如p-lists和UserDefaults
)一起使用
let users: [User] = try UserDefaults.standard["users"]^
有关更多信息,请参阅用法部分。
内容
需求
- Xcode
9.3+
- Swift 4.1
- 平台
- iOS
9.0+
- macOS
10.11+
- tvOS
9.0+
- watchOS
2.0+
- Linux
- iOS
安装
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 也没有 API 用于动态构建和解析 JSON(而不通过模型对象),这在构建和解析网络请求时很常见。因此,我们认为第三方 JSON 库的需求还将在一段时间内存在。
用法
介绍
让我们从一个简单的例子开始,这个例子展示了如何从一些类似于 JSON 的键值结构数据中提取数据
let jar: Jar = ["name": "Lift", "version": 1.0]
let name: String = try jar["name"]^
let version: Double = try jar["version"]^
Jar
是 Lift 的异构值容器。在这个例子中,它存储了一个字典。操作符 ^
(称为提升操作符)用于从 jar 容器中提取值。因为 jar 通常包含在编译时未知值的值,提取它们可能会失败。这可能会导致值缺失,如果值不是预期的类型,或者如果某些其他验证失败。这就是为什么你总是会看到存在提升操作符 ^
时有一个 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
是一个具有值语义的值类型,当将其声明为 var
时,您可以修改您的 Jar
值。
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
永不绑定到特定类型或值,因此更改它是Always ok的。
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 支持 primitive 类型数组的处理。
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-lists。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库包括对最常见的原始类型的扩展,如Int
、Bool
、String
等,通过将它们符合到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
要求您实现必要的初始化方法。当您与非最终类(例如从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会尝试跟踪与世界最接近的上下文以及您的数据的“key-path”,并将这些暴露在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工作空间与我们合作。提问、分享想法或者可能只是参与当前的讨论。要获得邀请,请给我们写信至 [email protected]
了解更多
要了解Lift的API如何演变,我们建议阅读以下文章: