Bender 2.0.0

Bender 2.0.0

测试已测试
语言语言 SwiftSwift
许可 MIT
发布最近发布2020年1月
SPM支持SPM

Evgeny Kamyshanov维护。



Bender 2.0.0

  • Evgeny Kamyshanov

Bender

DUB CocoaPods Carthage

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