Crust 0.13.0

Crust 0.13.0

测试已测试
Lang语言 SwiftSwift
许可证 MIT
发布最后发布2020年3月
SPM支持SPM

Rex Fenley 维护。



Crust 0.13.0

  • rexmas

CocoaPods Compatible Build Status

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 - 不需要 PersistanceAdapterMapping

对于每个模型,可以创建不限数量的不同用例的 MappingPersistanceAdapter

JSONValue用于安全JSON类型

Crust依赖于JSONValue进行其JSON编解码机制。它提供了许多好处,包括类型安全、索引操作和通过协议进行扩展。

如何进行映射

  1. 创建一组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
            }
        }
    }
  2. 使用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)
        }
    }
  3. 创建您的Crust映射器。

    let mapper = Mapper()
  4. 使用映射器将对象转换为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
  • 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

特此免费许可任何获得此软件和相关文档副本(“软件”)的人,未经限制地处理该软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本,并允许将软件提供给任何由软件提供的人员这样做,受以下条件约束

上述版权声明和本许可声明应包含在软件的所有副本或实质部分中。

软件按“原样”提供,不提供任何明示或暗示的保证,包括但不限于适销性、用于特定目的和不侵犯版权。在任何事件中,作者或版权持有人不对任何索赔、损害或其他责任负责,无论是基于合同、侵权或其他方式,源于、源于或与该软件的使用或以任何其他方式使用有关。