Crust
一个灵活的Swift框架,用于在类和结构体之间进行JSON的转换,并支持如Realm等存储解决方案。
🎸
功能
要求
iOS 10.0+ Swift 5.0+
对于iOS 8,请查看swift-3
标签(0.6.0..<0.7.0)对于Swift 3,请查看swift-4.0
标签,对于Swift 4.0,请查看swift-4.2
标签
安装
CocoaPods
platform :ios, '10.0'
use_frameworks!
pod 'Crust'
Swift Package Manager (SPM)
dependencies: [
.package(url: "https://github.com/rexmas/Crust.git", .upToNextMinor(from: "0.13.0"))
]
结构体和类
可以映射到或从类或结构体。
class Company {
var employees = Array<Employee>()
var uuid: String = ""
var name: String = ""
var foundingDate: NSDate = NSDate()
var founder: Employee?
var pendingLawsuits: Int = 0
}
如果你不需要存储(通常结构体使用),可以使用AnyMappable
。
struct Person: AnyMappable {
var bankAccounts: Array<Int> = [ 1234, 5678 ]
var attitude: String = "awesome"
var hairColor: HairColor = .Unknown
var ownsCat: Bool? = nil
}
关注点的分离
Crust 设计上考虑了关注点分离。它不对用户如何映射到和从 JSON 以及用户如何存储模型做出任何假设。
Crust 有 2 个基本协议
Mapping
- 如何将 JSON 映射到特定模型或从特定模型映射 - (当映射到对象的序列时,使用associatedtype MappedObject
设置模型,如果设置为序列的关联类型,则为associatedtype SequenceKind
)。 - 可能包括主键和嵌套映射。PersistanceAdapter
- 如何存储和检索用于映射的后端存储(例如 Core Data、Realm 等)中使用的模型对象。
当不需要 PersistanceAdapter
存储时,还有 2 个附加协议
AnyMappable
- 用来映射到 JSON 的模型(类或结构体)继承。AnyMapping
- 不需要PersistanceAdapter
的Mapping
。
对于每个模型,可以创建不限数量的不同用例的 Mapping
和 PersistanceAdapter
。
JSONValue用于安全JSON类型
Crust依赖于JSONValue进行其JSON编解码机制。它提供了许多好处,包括类型安全、索引操作和通过协议进行扩展。
如何进行映射
-
创建一组
MappingKey
,这些键路径定义了从JSON有效载荷到您模型的键路径。enum EmployeeKey: MappingKey { case uuid case name case employer(Set<CompanyKey>) var keyPath: String { switch self { case .employer(_): return "company" case .uuid: return "data.uuid" // This means our JSON has a 'data' payload we're elevating. case .name: return "data.name" } } // You can specifically specify what keys you'd like to map from in the `keyedBy` argument of the mapper. This function retrieves the nested keys. func nestedMappingKeys<Key: MappingKey>() -> AnyKeyCollection<Key>? { switch self { case .employer(let companyKeys): return companyKeys.anyKeyCollection() default: return nil } } } enum CompanyKey: MappingKey { case uuid case name case employees(Set<EmployeeKey>) case founder(Set<EmployeeKey>) case foundingDate case pendingLawsuits var keyPath: String { switch self { case .uuid: "uuid" case .name: "name" case .employees(_): "employees" case .founder(_): "founder" case .foundingDate: "data.founding_date" case .pendingLawsuits: "data.lawsuits_pending" } } func nestedMappingKeys<Key: MappingKey>() -> AnyKeyCollection<Key>? { switch self { case .employees(let employeeKeys): return employeeKeys.anyKeyCollection() case .founder(let employeeKeys): return employeeKeys.anyKeyCollection() default: return nil } } }
-
使用
Mapping
创建您的模型映射(如果带有存储)或使用AnyMapping
(如果无存储)。带有存储(假设
CoreDataAdapter
符合PersistanceAdapter
)class EmployeeMapping: Mapping { var adapter: CoreDataAdapter var primaryKeys: [Mapping.PrimaryKeyDescriptor]? { // property == attribute on the model, keyPath == keypath in the JSON blob, transform == tranform to apply to data from JSON blob. return [ (property: "uuid", keyPath: EmployeeKey.uuid.keyPath, transform: nil) ] } required init(adapter: CoreDataAdapter) { self.adapter = adapter } func mapping(inout toMap: inout Employee, payload: MappingPayload<EmployeeKey>) throws { // Company must be transformed into something Core Data can use in this case. let companyMapping = CompanyTransformableMapping() // No need to map the primary key here. toMap.employer <- (.mapping(.employer([]), companyMapping), payload) toMap.name <- (.name, payload) } }
无存储
class CompanyMapping: AnyMapping { // associatedtype MappedObject = Company is inferred by `toMap` func mapping(inout toMap: inout Company, payload: MappingPayload<CompanyKey>) throws { let employeeMapping = EmployeeMapping(adapter: CoreDataAdapter()) toMap.employees <- (.mapping(.employees([]), employeeMapping), payload) toMap.founder <- (.mapping(.founder([]), employeeMapping), payload) toMap.uuid <- (.uuid, payload) toMap.name <- (.name, payload) toMap.foundingDate <- (.foundingDate, payload) toMap.pendingLawsuits <- (.pendingLawsuits, payload) } }
-
创建您的Crust映射器。
let mapper = Mapper()
-
使用映射器将对象转换为
JSONValue
对象或将JSONValue
对象转换回对象let json = try! JSONValue(object: [ "uuid" : "uuid123", "name" : "name", "employees" : [ [ "data" : [ "name" : "Fred", "uuid" : "ABC123" ] ], [ "data" : [ "name" : "Wilma", "uuid" : "XYZ098" ] ] ] "founder" : NSNull(), "data" : [ "lawsuits_pending" : 5 ], // Works with '.' keypaths too. "data.founding_date" : NSDate().toISOString(), ] ) // Just map 'uuid', 'name', 'employees.name', 'employees.uuid' let company: Company = try! mapper.map(from: json, using: CompanyMapping(), keyedBy: [.uuid, .name, .employees([.name, .uuid])]) // Or if json is an array and you'd like to map everything. let company: [Company] = try! mapper.map(from: json, using: CompanyMapping(), keyedBy: AllKeys())
注意:JSONValue
可以转换为json的AnyObject
变体,通过json.values()
,也可以通过try! json.encode()
转换为NSData
。
嵌套映射
Crust支持嵌套模型的嵌套映射,例如上面的示例
func mapping(inout toMap: Company, payload: MappingPayload<CompanyKey>) throws {
let employeeMapping = EmployeeMapping(adapter: CoreDataAdapter())
toMap.employees <- (Binding.mapping(.employees([]), employeeMapping), payload)
}
绑定和集合
Binding
在映射集合时提供专用的指令。使用.collectionMapping
情况通知映射器这些指令。它们包括
- 替换和/或删除对象
- 向集合中添加对象
- 集合中的唯一对象(合并重复项)
- 在唯一化期间,最新映射的属性会覆盖现有对象的属性。未映射的属性保持不变。
- 如果映射到的集合的
Element
遵循Equatable
,则唯一化会自动进行。 - 如果
Element
不遵循Equatable
,则除非显式提供UniquingFunctions
且使用了映射函数map(toCollection field:, using binding:, uniquing:)
,否则唯一化将被忽略。
- 接受来自集合的"null"值映射。
此表提供了一些根据要映射到的集合类型和给定nullable
以及JSON有效载荷中是否存在值或"null"值来映射"null" JSON值的一些示例。
追加/替换 | 可以为null | 值/空 | 数组 | 数组? | RLMArray |
---|---|---|---|---|---|
append | 是或否 | vals | append | append | append |
append | 是 | null | 不执行操作 | 不执行操作 | 不执行操作 |
替换 | 是或否 | vals | 替换 | 替换 | 替换 |
替换 | 是 | null | 移除所有项 | 分配null | 移除所有项 |
追加或替换 | 否 | null | 错误 | 错误 | 错误 |
默认情况下,使用.mapping
将(插入: .replace(delete: nil), unique: true, nullable: true)
。
public enum CollectionInsertionMethod<Container: Sequence> {
case append
case replace(delete: ((_ orphansToDelete: Container) -> Container)?)
}
public typealias CollectionUpdatePolicy<Container: Sequence> =
(insert: CollectionInsertionMethod<Container>, unique: Bool, nullable: Bool)
public enum Binding<M: Mapping>: Keypath {
case mapping(Keypath, M)
case collectionMapping(Keypath, M, CollectionUpdatePolicy<M.SequenceKind>)
}
用法
let employeeMapping = EmployeeMapping(adapter: CoreDataAdapter())
let binding = Binding.collectionMapping("", employeeMapping, (.replace(delete: nil), true, true))
toMap.employees <- (binding, payload)
详细信息请参考./Mapper/MappingProtocols.swift。
映射负载
每个mapping
都通过一个Payload: MappingPayload<T>
进行传递,这在映射过程中必须包括。该payload
包括从映射回调传播的错误信息以及被映射到的json和对象的上下文信息。
要在映射过程中包括负载,请将其作为元组包含。
func mapping(inout toMap: Company, payload: MappingPayload<CompanyKey>) throws {
toMap.uuid <- (.uuid, payload)
toMap.name <- (.name, payload)
}
自定义转换
要创建一个简单的自定义转换(例如基本值类型),实现Transform
协议
public protocol Transform: AnyMapping {
func fromJSON(_ json: JSONValue) throws -> MappedObject
func toJSON(_ obj: MappedObject) -> JSONValue
}
مثل的其他任何Mapping
。
为同一模型提供不同的映射
允许相同的模型有多个Mapping
。
class CompanyMapping: AnyMapping {
func mapping(inout toMap: Company, payload: MappingPayload<CompanyKey>) throws {
toMap.uuid <- (.uuid, payload)
toMap.name <- (.name, payload)
}
}
class CompanyMappingWithNameUUIDReversed: AnyMapping {
func mapping(inout toMap: Company, payload: MappingPayload<CompanyKey>) throws {
toMap.uuid <- (.name, payload)
toMap.name <- (.uuid, payload)
}
}
只需使用两个不同的映射。
let mapper = Mapper()
let company1 = try! mapper.map(from: json, using: CompanyMapping(), keyedBy: AllKeys())
let company2 = try! mapper.map(from: json, using: CompanyMappingWithNameUUIDReversed(), keyedBy: AllKeys())
持久性适配器
遵循PersistanceAdapter
协议将数据存储到Core Data、Realm等。
符合PersistanceAdapter
协议的对象必须包含两个associatedtype
:
BaseType
- 此存储系统模型对象的顶层类。- 对于Core Data,这将对应于
NSManagedObject
。 - 对于Realm,这将对应于
RLMObject
。 - 对于RealmSwift,这将对应于
Object
。
- 对于Core Data,这将对应于
ResultsType: Collection
- 用于对象查找。应设置为BaseType
的集合。
然后,Mapping
必须将其associatedtype AdapterKind = <Your Adapter>
设置为在映射过程中使用它。
区域
在 ./RealmCrustTests
中包含了使用 Crust 与 realm-cocoa(Obj-C)的示例。
如果您想使用 Crust 与 RealmSwift,可以查看这个(略过时)的仓库示例。 https://github.com/rexmas/RealmCrust
贡献
欢迎提交拉取请求!
- 如果您遇到任何问题,请创建一个问题。
- 从项目分叉并提交拉取请求以贡献。请包含新代码的测试。
- 保持 Linux 测试更新
swift test --generate-linuxmain
许可证
MIT 许可证(MIT)
版权所有 (c) 2015-2018 Rex
特此免费许可任何获得此软件和相关文档副本(“软件”)的人,未经限制地处理该软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本,并允许将软件提供给任何由软件提供的人员这样做,受以下条件约束
上述版权声明和本许可声明应包含在软件的所有副本或实质部分中。
软件按“原样”提供,不提供任何明示或暗示的保证,包括但不限于适销性、用于特定目的和不侵犯版权。在任何事件中,作者或版权持有人不对任何索赔、损害或其他责任负责,无论是基于合同、侵权或其他方式,源于、源于或与该软件的使用或以任何其他方式使用有关。