Bender
Bender是一个声明式的JSON映射库,它不会将可笑的初始化器和其他东西污染你的模型。为你的类描述JSON,而不是为你的类装扮JSON。
Bender
- 专注于描述JSON数据,就像JSON schema做的那样;
- 不会使你的模型依赖于库;
- 支持通过错误抛出强制/可选字段检查;
- 支持所有原生的JSON字段类型的类和结构体,包括递归嵌套的、作为字段或JSON根的数组,自定义枚举等;
- 支持JSON路径;
- 使用相同的验证规则将类转回JSON;
- 允许你用几十行代码编写自己的验证器/输出器;
- 体积小:Swift中 ~600 行代码;
- 非常快(参见包含的性能测试)!
示例
让我们假设我们收到了一个如下JSON结构
{
"title": "root",
"size": 128,
"folders": [
{
"title": "home",
"size": 256
}
]
}
这里我们有递归嵌套相同类型的结构体。我们想将这些数据映射到我们已有的Folder类
class Folder {
var name: String!
var size: Int64!
var folders: [Folder]?
}
我们如何检查是否得到了正确的数据?Bender帮助我们描述我们的期望。让我们从一个简单的表达式开始
let folderRule = ClassRule(Folder())
.expect("title", StringRule) { $0.name = $1 }
.expect("size", Int64Rule) { $0.size = $1 }
这意味着什么?我们实际上创建了一条规则,一个描述我们期望在JSON中的模式的方案:一个包含两个强制字段的struct(ClassRule),其中一个字段是String类型的,命名为"title"(StringRule),另一个字段是Int64类型的,命名为"size"(Int64Rule)。但最终我们希望将可以从这些字段中提取的值绑定到相应的class Folder的字段。
ClassRule获取@autoclosure
,这样每次我们验证相应的JSON片段时,都会构造一个新的Folder对象。如果验证失败,绑定对象不会被创建。
在类似 { $0.name = $1 }
的绑定闭包中,我们传递文件夹对象引用作为 $0 参数,并将从 JSON 中提取的字段 "name" 的值作为 $1。具体在这里调用可绑定项的哪种方法取决于您。这里可以有适配器、编码器、解码器、转换器等,不仅仅是简单的赋值。
该规则可能只声明一次,但我们在有新的 JSON 对象的地方都可以使用它。
let folder = try folderRule.validate(jsonObject) // the resulting 'folder' will be of type Folder
等等。嵌套文件夹怎么办?没问题。只需在我们的规则中再添加一个字段期望:一个 可选 数组。这个数组中的元素可以用我们正在声明的相同规则进行检查,递归地进行。
folderRule.optional("folders", ArrayRule(itemRule: folderRule)) { $0.folders = $1 }
validate
是如何工作的?它会尝试在 JSON 中找到必填字段,如果成功,则根据给定的绑定规则绑定它们。如果任何一个必填规则找不到适当的字段,或者字段本身无法进行验证,将抛出异常,并且绑定不会发生。然后检查所有可选字段,如果找到了任何未通过验证的字段,同样会抛出异常。
当然,我们可以使用相同的规则将文件夹类转储为 JSON 对象。我们只需添加相应的数据访问器即可。
let folderRule = ClassRule(Folder())
.expect("title", StringRule, { $0.name = $1 }) { $0.name }
.expect("size", Int64Rule, { $0.size = $1 }) { $0.size }
folderRule
.optional("folders", ArrayRule(itemRule: folderRule), { $0.folders = $1 }) { $0.folders }
现在我们可以使用这个规则对文件夹类进行序列化了。
let jsonObject = try folderRule.dump(folder)
规则列表
基本规则
- IntRule, Int8Rule, Int16Rule, Int32Rule, Int64Rule(以及相应的 UInt... 系列规则)
- DoubleRule
- FloatRule
- BoolRule
- StringRule
复合规则
- ClassRule - 绑定类
- StructRule - 绑定结构体
- EnumRule - 将枚举绑定到任何值集合
带有嵌套规则的规则
- ArrayRule - 绑定其他类型的数组,并由项目规则进行验证
- StringifiedJSONRule - 将任何规则从 JSON 编码为 UTF-8 字符串进行绑定
错误处理
Bender 在验证或转储出错时抛出 RuleError 枚举,它存储有关错误原因的可选信息。
假设我们有一个有错误的 JSON,其中一个整数值变成了一个字符串
{
"title": "root",
"size": 128,
"folders": [
{
"title": "home",
"size": "256 Error!"
}
]
}
验证将抛出 RuleError,您可以通过 error.description
获取错误描述
Unable to validate optional field "folders" for Folder.
Unable to validate array of Folder: item #0 could not be validated.
Unable to validate mandatory field "size" for Folder.
Value of unexpected type found: "256 Error!". Expected Int64.
在某些情况下,我们应该允许世界并不完美。比如说,我们在数组中找到一个黑羊。我们应该因为一个小项目出了错而使整个数组的验证失败吗?有时候不。只需声明 'invalidItemHandler' 闭包。
let someArrayRule = ArrayRule(itemRule: someRule) {
print("Error: \($0)")
// If you still want to throw an error here, you can. Just do it:
// throw TheError("I am sure this is an unrecoverable error: \($0)")
}
如果您的 'invalidItemHandler' 仍然抛出异常,则在项目验证错误的情况下,整个数组的验证将失败。
结构体支持
Swift 结构体也支持作为可绑定项。例如,如果我们的 Folder
是结构体,而不是类,我们仍然可以使用几乎相同的 StructRule
来绑定它。
let folderRule = StructRule(ref(Folder(name: "", size: 0)))
.expect("title", StringRule, { $0.value.name = $1 }) { $0.name }
.expect("size", Int64Rule, { $0.value.size = $1 }) { $0.size }
folderRule
.optional("folders", ArrayRule(itemRule: folderRule), { $0.value.folders = $1 }) { $0.folders }
你注意到额外的 ref
吗?它是一个装箱的 object,允许我们在验证的规则集通过值复制结构体作为引用传递。同样,在我们的 bind 封闭调用中,我们应该通过调用 $0.value
来取消装箱,它返回可变文件夹结构体。
你甚至可以将 JSON 结构体绑定到元组中!为此也可以使用 StructRule。
let folderRule = StructRule(ref(("", 0)))
.expect("title", StringRule, { $0.value.0 = $1 }) { $0.0 }
.expect("size", Int64Rule, { $0.value.1 = $1 }) { $0.1 }
let newJson = try folderRule.dump(("home dir", 512))
let tuple = try folderRule.validate(json) // 'tuple' will be of type (String, Int64)
JSON 路径
有时你不需要绑定任何中间的 JSON 字典。例如,你想从像这样的 JSON 中提取只包含 'user' 结构体的内容
{
"message": {
"payload": {
"createdBy": {
"user": {
"id": "123456",
"login": "[email protected]"
}
}
}
}
}
你不需要为所有这些中间内容创建冗余的类。只需使用神奇的运算符 "/" 来构建所需的路径
let rule = ClassRule(User())
.expect("message"/"payload"/"createdBy"/"user"/"id", StringRule) { $0.id = $1 }
.expect("message"/"payload"/"createdBy"/"user"/"login", StringRule) { $0.name = $1 }
核心数据
你的受管理对象也可以很容易地进行映射。让我们想象一下深受喜爱的 Employee/Department 模式,但比平时稍复杂:Employee 和 Department 通过弱引用、部门名称相互连接。
所以,我们先来定义 Employee...
{
"name": "John Doe",
"departmentName": "Marketing"
}
…然后是 Department
{
"name": "Marketing"
}
同时,我们的 Core Data 模式可以是传统的模式(这里省略了一些无聊的 boilerplate Core Data 代码)
class Employee: NSManagedObject {
@NSManaged var name: String
@NSManaged var department: Department?
}
class Department: NSManagedObject {
@NSManaged var name: String
@NSManaged var employees: NSSet
}
func createEmployee(context: NSManagedObjectContext) -> Employee {
/// ... All that 'NSEntityDescription' and 'NSManagedObject' stuff
}
func createDepartment(context: NSManagedObjectContext) -> Department {
/// ... All that 'NSEntityDescription' and 'NSManagedObject' stuff
}
现在是时候创建相应的规则了。但是对象工厂依赖于运行时环境。因此,我们可以使用简单的函数封装我们的规则创建代码
func departmentByName(context: NSManagedObjectContext, name: String) -> Department? {
/// ... Searches for department by its name
}
func employeeRule(context: NSManagedObjectContext) -> ClassRule<Employee> {
return ClassRule(createEmployee(context))
.expect("name", StringRule) { $0.name = $1 }
.optional("departmentName", StringRule) {
if let dept = departmentByName(context, name: $1) {
$0.department = dept
dept.mutableSetValueForKey("employees").addObject($0)
}
}
}
func departmentRule(context: NSManagedObjectContext) -> ClassRule<Department> {
return ClassRule(createDepartment(context))
.expect("name", StringRule) { $0.name = $1 }
}
现在验证变得简单起来
try departmentRule(context).validate(deptJson) // here we have Department with name "Marketing" created
try employeeRule(context).validate(employeeJson) // here we have Employee mapped to corresponding Department
可扩展性
你可以在系统中添加自己的规则。你所需要做的就是遵循非常简单的 Rule
协议
public protocol Rule {
typealias V
func validate(jsonValue: AnyObject) throws -> V
func dump(value: V) throws -> AnyObject
}
安装
CocoaPods
pod 'Bender', '~> 2.0.0'
Carthage
github "ptiz/Bender" == 2.0.0