CocoaPods Compatible Build Status

Crust

一个灵活的 Swift 框架,用于类和结构体与 JSON 之间的转换,并支持诸如 Realm 等存储解决方案。

特性 🎸

要求

iOS 10.0+ Swift 5.0+

对于 iOS 8 使用 v 0.12.0,请参阅 swift-3 标签。对于 Swift 3 使用 (v 0.6.0..<0.7.0),请参阅 swift-4.0 标签。对于 Swift 4.0,请参阅 swift-4.2 标签。对于 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 映射和映射到 JSON 的方式数量以及用户希望存储其模型的各种方式做出任何假设。

Crust 有 2 个基本协议

当不需要存储 PersistanceAdapter 时,还有 2 个额外的协议

对于不同的用例,每个模型可以创建的各种 MappingPersistanceAdapter 的数量没有限制。

用于类型安全 JSON 的 JSONValue

Crust 依赖于 JSONValue 作为其 JSON 编码和解码机制。 它提供了许多优点,包括类型安全、下标和通过协议的可扩展性。

如何映射

  1. 创建一组 MappingKey,用于定义从 JSON payload 到模型的键路径。

    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 Mapper。

    let mapper = Mapper()
  4. 使用 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())

注意: 可以通过 json.values()JSONValue 转换回 AnyObject 类型的 json 变体,并通过 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 case 通知 mapper 这些指令。 它们包括

下表提供了一些示例,说明如何根据被映射到的 Collection 类型以及 nullable 的值以及 JSON payload 中是否存在值或 "null" 值来映射 "null" json 值。

append / replace nullable vals / null Array Array? RLMArray
append yes or no vals append append append
append yes null no-op no-op no-op
replace yes or no vals replace replace replace
replace yes null removeAll assign null removeAll
append or replace no null error error error

默认情况下,使用 .mapping(insert: .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。

映射 Payload

每个 mapping 都传递一个 Payload: MappingPayload<T>,该 Payload 必须在映射期间包含。 payload 包括从映射传播回调用方的错误信息以及有关被映射到/从 JSON 映射到对象的上下文信息。

要在映射期间包含 payload,请将其作为元组包含。

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

然后 Mapping 必须设置其 associatedtype AdapterKind = <Your Adapter> 以在映射期间使用它。

Realm

./RealmCrustTests 中包含一些测试,其中包含如何将 Crust 与 realm-cocoa (Obj-C) 一起使用的示例。

如果您希望将 Crust 与 RealmSwift 一起使用,请查看此(稍微过时的)存储库以获取示例。 https://github.com/rexmas/RealmCrust

贡献

欢迎 pull requests!

许可证

MIT 许可证 (MIT)

版权所有 (c) 2015-2018 Rex

特此授予任何人免费获得本软件及其相关文档文件(“软件”)的副本的权利,以不受限制地处理本软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或出售本软件的副本的权利,并允许向其提供本软件的人员这样做,但须符合以下条件

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

本软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于适销性、特定用途适用性和非侵权性的保证。 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是在合同、侵权行为或其他方面,由本软件或本软件的使用或其他处理引起的或与之相关的。