Vox
Vox是Swift JSONAPI标准的实现。
背后的魔法
Vox结合了Swift的动态性和C选择器的Objective-C。在序列化和反序列化过程中,JSON不会映射到资源对象(s)。取而代之的是,它使用封送和解封送技术来解决直接内存访问和性能挑战。代理(代表)设计模式给我们提供了直接通过类属性(属性)操纵JSON值并反之亦然的机会。
import Vox
class Person: Resource {
@objc dynamic var name: String?
}
let person = Person()
person.name = "Sherlock Holmes"
print(person.attributes?["name"]) // -> "Sherlock Holmes"
让我们解释一下幕后发生了什么!
-
设置人的名字不会将值分配给
Person
对象。相反,它将直接修改其背后的JSON(从服务器接收的)。 -
获取属性实际上会解决JSON中的值(它指向其实际内存地址)。
-
当直接更改资源中的
attributes
或relationship
字典的值时,获取属性值将解析为JSON中更改的值。
每个属性或关系(Resource
子类属性)都必须具有@objc dynamic
前缀才能这样做。
将您的
Resource
类视为对 JSON 对象的强类型接口。
这为轻松处理以下情况提供了可能性:
- I/O 性能
- 多态关系
- 有循环引用的关系
- 从包含列表中懒加载资源
安装
要求
- Xcode 9
- CocoaPods
基本
pod 'Vox'
包含 Alamofire 插件
pod 'Vox/Alamofire'
用法
定义资源
import Vox
class Article: Resource {
/*--------------- Attributes ---------------*/
@objc dynamic
var title: String?
@objc dynamic
var descriptionText: String?
@objc dynamic
var keywords: [String]?
@objc dynamic
var viewsCount: NSNumber?
@objc dynamic
var isFeatured: NSNumber?
@objc dynamic
var customObject: [String: Any]?
/*------------- Relationships -------------*/
@objc dynamic
var authors: [Person]?
@objc dynamic
var editor: Person?
/*------------- Resource type -------------*/
// resource type must be defined
override class var resourceType: String {
return "articles"
}
/*------------- Custom coding -------------*/
override class var codingKeys: [String : String] {
return [
"descriptionText": "description"
]
}
}
序列化
单个资源
import Vox
let person = Person()
person.name = "John Doe"
person.age = .null
person.gender = "male"
person.favoriteArticle = .null()
let json: [String: Any] = try! person.documentDictionary()
// or if `Data` is needed
let data: Data = try! person.documentData()
上一个示例将解析为以下 JSON
{
"data": {
"attributes": {
"name": "John Doe",
"age": null,
"gender": "male"
},
"type": "persons",
"id": "id-1",
"relationships": {
"favoriteArticle": {
"data": null
}
}
}
}
在此示例中,喜欢的文章从人那里未分配。为此,在资源属性上使用 .null()
,在其他所有属性上使用 .null
。
资源收集
import Vox
let article = Article()
article.id = "article-identifier"
let person1 = Person()
person1.id = "id-1"
person1.name = "John Doe"
person1.age = .null
person1.gender = "male"
person1.favoriteArticle = article
let person2 = Person()
person2.id = "id-2"
person2.name = "Mr. Nobody"
person2.age = 99
person2.gender = .null
person2.favoriteArticle = .null()
let json: [String: Any] = try! [person1, person2].documentDictionary()
// or if `Data` is needed
let data: Data = try! [person1, person2].documentData()
上一个示例将解析为以下 JSON
{
"data": [
{
"attributes": {
"name": "John Doe",
"age": null,
"gender": "male"
},
"type": "persons",
"id": "id-1",
"relationships": {
"favoriteArticle": {
"data": {
"id": "article-identifier",
"type": "articles"
}
}
}
},
{
"attributes": {
"name": "Mr. Nobody",
"age": 99,
"gender": null
},
"type": "persons",
"id": "id-2",
"relationships": {
"favoriteArticle": {
"data": null
}
}
}
]
}
空值
在 Resource
类型的属性上使用 .null()
或在任何其他类型属性上使用 .null
。
- 将属性值设置为
.null
(或.null()
)将使 JSON 值设置为null
- 将属性值设置为
nil
将从 JSON 中移除值
反序列化
单个资源
import Vox
let data: Data // -> provide data received from JSONAPI server
let deserializer = Deserializer.Single<Article>()
do {
let document = try deserializer.deserialize(data: self.data)
// `document.data` is an Article object
} catch JSONAPIError.API(let errors) {
// API response is valid JSONAPI error document
errors.forEach { error in
print(error.title, error.detail)
}
} catch JSONAPIError.serialization {
print("Given data is not valid JSONAPI document")
} catch {
print("Something went wrong. Maybe `data` does not contain valid JSON?")
}
资源集合
import Vox
let data: Data // -> provide data received from JSONAPI server
let deserializer = Deserializer.Collection<Article>()
let document = try! deserializer.deserialize(data: self.data)
// `document.data` is an [Article] object
描述
提供的数据必须是包含有效 JSONAPI 文档或错误的 Data
对象。如果未满足这些先决条件,将抛出 JSONAPIError.serialization
错误。
反序列化器也可以不声明泛型,但在此情况下,资源的 data
属性可能需要在您的侧进行强制转换,因此建议使用泛型。
Document<DataType: Any>
有以下属性
属性 | 类型 | 描述 |
---|---|---|
data |
DataType |
包含单个资源或资源集合 |
meta |
[String: Any] |
meta 字典 |
jsonapi |
[String: Any] |
jsonApi 字典 |
links |
链接 |
Links 对象,例如,可以包含分页数据 |
included |
[[String: Any]] |
included 字典数组 |
网络
可以在路径字符串中使用<id>
和<type>
注释。如果可能,它们将用适当的值替换。
客户端协议
实现以下来自Client
协议的方法
func executeRequest(path: String,
method: String,
queryItems: [URLQueryItem],
bodyParameters: [String : Any]?,
success: @escaping ClientSuccessBlock,
failure: @escaping ClientFailureBlock,
userInfo: [String: Any])
其中
ClientSuccessBlock
=(HTTPURLResponse?, Data?) -> Void
ClientFailureBlock
=(Error?, Data?) -> Void
注意
userInfo
包含您可以传递给客户端以执行一些自定义逻辑的自定义数据:例如添加一些额外的标题,添加加密等。
Alamofire客户端插件
如果不需要自定义网络,有一个插件包装了Alamofire并提供了符合JSON:API规范的网络客户端。
Alamofire是Swift中的优雅HTTP网络
示例
let baseURL = URL(string: "http://demo7377577.mockable.io")!
let client = JSONAPIClient.Alamofire(baseURL: baseURL)
let dataSource = DataSource<Article>(strategy: .path("vox/articles"), client: client)
dataSource
.fetch()
...
安装
pod 'Vox/Alamofire'
获取单个资源
let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>/<id>"), client: client)
dataSource
.fetch(id:"1")
.include([
"favoriteArticle"
])
.result({ (document: Document<Person>) in
let person = document?.data // ➜ `person` is `Person?` type
}) { (error) in
if let error = error as? JSONAPIError {
switch error {
case .API(let errors):
()
default:
()
}
}
}
获取资源集合
let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
dataSource(url: url)
.fetch()
.include([
"favoriteArticle"
])
.result({ (document: Document<[Person]>) in
let persons = document.data // ➜ `persons` is `[Person]?` type
}) { (error) in
}
创建资源
let person = Person()
person.id = "1"
person.name = "Name"
person.age = 40
person.gender = "female"
let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
dataSource
.create(person)
.result({ (document: Document<Person>?) in
let person = document?.data // ➜ `person` is `Person?` type
}) { (error) in
}
更新资源
let person = Person()
person.id = "1"
person.age = 41
person.gender = .null
let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>/<id>"), client: client)
dataSource
.update(resource: person)
.result({ (document: Document<Person>?) in
let person = document?.data // ➜ `person` is `Person?` type
}) { (error) in
}
删除资源
let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>/<id>"), client: client)
dataSource
.delete(id: "1")
.result({
}) { (error) in
}
分页
初始化请求分页
自定义分页策略
let paginationStrategy: PaginationStrategy // -> your object conforming `PaginationStrategy` protocol
let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
dataSource
.fetch()
.paginate(paginationStrategy)
.result({ (document) in
}, { (error) in
})
基于页面分页策略
let paginationStrategy = Pagination.PageBased(number: 1, size: 10)
let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
dataSource
.fetch()
.paginate(paginationStrategy)
.result({ (document) in
}, { (error) in
})
基于偏移的分页策略
let paginationStrategy = Pagination.OffsetBased(offset: 10, limit: 10)
let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
dataSource
.fetch()
.paginate(paginationStrategy)
.result({ (document) in
}, { (error) in
})
基于游标的分页策略
let paginationStrategy = Pagination.CursorBased(cursor: "cursor")
let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
dataSource
.fetch()
.paginate(paginationStrategy)
.result({ (document) in
}, { (error) in
})
将下一页添加到当前文档
document.appendNext({ (data) in
// data.old -> Resource values before pagination
// data.new -> Resource values from pagination
// data.all -> Resource values after pagination
// document.data === data.all -> true
}, { (error) in
})
获取下一个文档页面
document.next?.result({ (nextDocument) in
// `nextDocument` is same type as `document`
}, { (error) in
})
获取前一个文档页面
document.previous?.result({ (previousDocument) in
// `previousDocument` is same type as `document`
}, { (error) in
})
获取第一个文档页面
document.first?.result({ (firstDocument) in
// `firstDocument` is same type as `document`
}, { (error) in
})
获取最后一个文档页面
document.last?.result({ (lastDocument) in
// `lastDocument` is same type as `document`
}, { (error) in
})
重新加载当前文档页面
document.reload?.result({ (reloadedDocument) in
// `reloadedDocument` is same type as `document`
}, { (error) in
})
自定义路由
为资源生成URL可以自动化。
创建一个新的符合Router
的对象。简单示例
class ResourceRouter: Router {
func fetch(id: String, type: Resource.Type) -> String {
let type = type.resourceType
return type + "/" + id // or "<type>/<id>"
}
func fetch(type: Resource.Type) -> String {
return type.resourceType // or "<type>"
}
func create(resource: Resource) -> String {
return resource.type // or "<type>"
}
func update(resource: Resource) -> String {
let type = type.resourceType
return type + "/" + id // or "<type>/<id>"
}
func delete(id: String, type: Resource.Type) -> String {
let type = type.resourceType
return type + "/" + id // or "<type>/<id>"
}
}
然后你会使用
let router = ResourceRouter()
let dataSource = DataSource<Person>(strategy: .router(router), client: client)
dataSource
.fetch()
...
测试
- 在创建资源时,带有路由和客户端的数据源在客户端上执行请求
- 在创建资源时,带有路由和客户端的数据源在路由上调用正确的方法
- 在创建资源时,带有路由和客户端的数据源将正确的参数传递给路由
- 在创建资源时,客户端从路由接收正确数据以执行任务
- 在检索单个资源时,带有路由和客户端的数据源在客户端上执行请求
- 在检索单个资源时,带有路由和客户端的数据源在路由上调用正确的方法
- 在检索单个资源时,带有路由和客户端的数据源将正确的参数传递给路由
- 在检索单个资源时,客户端从路由接收正确数据以执行任务
- 在检索资源集合时,带有路由和客户端的数据源在客户端上执行请求
- 在检索资源集合时,带有路由和客户端的数据源在路由上调用正确的方法
- 在检索资源集合时,带有路由和客户端的数据源将正确的参数传递给路由
- 在检索资源集合时,客户端从路由接收正确数据以执行任务
- 在更新资源时,带有路由和客户端的数据源在客户端上执行请求
- 在更新资源时,带有路由和客户端的数据源在路由上调用正确的方法
- 在更新资源时,带有路由和客户端的数据源将正确的参数传递给路由
- 在更新资源时,客户端从路由接收正确数据以执行任务
- 在删除资源时,带有路由和客户端的数据源在客户端上执行请求
- 在删除资源时,带有路由和客户端的数据源在路由上调用正确的方法
- 在删除资源时,带有路由和客户端的数据源将正确的参数传递给路由
- 在删除资源时,客户端从路由接收正确数据以执行任务
- 在带有路径和客户端创建资源时在客户端上执行请求
- 在带有路径和客户端创建资源时,客户端接收用于执行的正确数据
- 在带有路径和客户端创建资源时,客户端接收userInfo用于执行
- 在带有路径和客户端检索单个资源时在客户端上执行请求
- 在带有路径和客户端检索单个资源时,客户端接收用于执行的正确数据
- 使用路径和客户端从资源集合中获取自定义分页数据时,数据源在客户端执行请求
- 使用路径和客户端从资源集合中获取自定义分页数据时,客户端接收到执行所需正确数据
- 使用路径和客户端从基于页面的分页获取资源集合时,数据源在客户端执行请求
- 使用路径和客户端从基于页面的分页获取资源集合时,客户端接收到执行所需正确数据
- 使用路径和客户端从基于偏移量的分页获取资源集合时,数据源在客户端执行请求
- 使用路径和客户端从基于偏移量的分页获取资源集合时,客户端接收到执行所需正确数据
- 使用路径和客户端从基于游标的分页获取资源集合时,数据源在客户端执行请求
- 使用路径和客户端从基于游标的分页获取资源集合时,客户端接收到执行所需正确数据
- 使用路径和客户端更新资源时,数据源在客户端执行请求
- 使用路径和客户端更新资源时,客户端接收到执行所需正确数据
- 使用路径和客户端删除资源时,数据源在客户端执行请求
- 使用路径和客户端删除资源时,客户端接收到执行所需正确数据
- 反序列化器在反序列化资源集合时进行正确映射
- 反序列化器在反序列化单个资源且提供了包含在源对象中的错误数据时,错误数据映射到错误对象
- 反序列化器在反序列化单个资源且提供了包含在源对象中的错误数据时,错误数据映射到错误对象 2
- 反序列化器在反序列化关系中的多态对象文档时进行正确映射
- 反序列化器在反序列化单个资源时进行正确映射
- 分页数据源在获取第一页时返回第一页文档
- 分页数据源在获取第一页时获取下一页返回下一页文档
- 分页数据源在获取第一页时返回第一页文档 2
- 分页数据源在获取第一页时获取文档的第一页返回第一页文档
- 分页数据源在获取第一页时返回第一页文档 3
- 分页数据源在获取第一页时追加下一页文档被追加
- 分页数据源在获取第一页时追加下一页包含的被追加
- 分页数据源在获取第一页时返回第一页文档 4
- 分页数据源在获取第一页时重新加载当前页面接收页面
- 分页数据源在获取第一页时返回第一页文档 5
- 分页数据源在获取第一页时获取上一页接收页面
- 分页数据源在获取第一页时返回第一页文档 6
- 分页数据源在获取第一页时获取最后一页返回最后一页文档
- 序列化器在序列化资源集合时进行正确映射
- 序列化器在序列化资源集合时返回文档数据
- 序列化器在序列化资源集合时返回文档字典
- 序列化器在序列化单个资源时进行正确映射
- 序列化器在序列化单个资源时返回文档数据
- 序列化器在序列化单个资源时返回文档字典
贡献
欢迎Pull Requests。对于重大更改,请首先打开一个issue来讨论您想做出的更改。
请确保根据需要更新测试。