class Company {
var employees = Array<Employee>()
var uuid: String = ""
var name: String = ""
var foundingDate: NSDate = NSDate()
var founder: Employee?
var pendingLawsuits: Int = 0
}
If you have no need for storage, which will generaly be the case for structs, use AnyMappable.
struct Person: AnyMappable {
var bankAccounts: Array<Int> = [ 1234, 5678 ]
var attitude: String = "awesome"
var hairColor: HairColor = .Unknown
var ownsCat: Bool? = nil
}
Separation of Concerns
By design Crust is built with separation of concerns in mind. It makes no assumptions about how many ways a user would like to map to and from JSON and how many various ways the user would like to store their models.
Crust has 2 basic protocols:
Mapping - How to map JSON to and from a particular model - (model is set by the associatedtype MappedObject if mapping to an sequence of objects set associatedtype SequenceKind). - May include primary key(s) and nested mapping(s).
PersistanceAdapter - How to store and retrieve model objects used for mapping from a backing store (e.g. Core Data, Realm, etc.).
And 2 additional protocols when no storage PersistanceAdapter is required:
AnyMappable - Inherited by the model (class or struct) to be mapped to and from JSON.
AnyMapping - A Mapping that does not require an PersistanceAdapter.
There are no limitations on the number of various Mappings and PersistanceAdapters one may create per model for different use cases.
JSONValue for type safe JSON
Crust relies on JSONValue for it’s JSON encoding and decoding mechanism. It offers many benefits including type safety, subscripting, and extensibility through protocols.
How To Map
Create a set of MappingKeys that define the key paths from the JSON payload to your model.
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
}
}
}
Create your mappings for your model using Mapping if with storage or AnyMapping if without storage.
With storage (assume CoreDataAdapter conforms to 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)
}
}
Binding provides specialized directives when mapping collections. Use the .collectionMapping case to inform the mapper of these directives. They include
replace and/or delete objects
append objects to the collection
unique objects in collection (merge duplicates)
The latest mapped properties overwrite the existing object’s properties during uniquing. Properties not mapped remain unchanged.
Uniquing works automatically if the Elements of the collection being mapped follow Equatable.
If the Elements do not follow Equatable then uniquing is ignored unless UniquingFunctions are explicitly provided and the mapping function map(toCollection field:, using binding:, uniquing:) is used.
Accept “null” values to map from the collection.
This table provides some examples of how “null” json values are mapped depending on the type of Collection being mapped to and given the value of nullable and whether values or “null” are present in the JSON payload.
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
By default using .mapping will (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>)
}
Usage:
let employeeMapping = EmployeeMapping(adapter: CoreDataAdapter())
let binding = Binding.collectionMapping("", employeeMapping, (.replace(delete: nil), true, true))
toMap.employees <- (binding, payload)
Look in ./Mapper/MappingProtocols.swift for more.
Mapping Payload
Every mapping passes through a Payload: MappingPayload<T> which must be included during the mapping. The payload includes error information that is propagated back from the mapping to the caller and contextual information about the json and object being mapped to/from.
To include the payload during mapping include it as a tuple.
Fork the project and submit a pull request to contribute. Please include tests for new code.
Keep Linux tests up-to-date swift test --generate-linuxmain
License
The MIT License (MIT)
Copyright (c) 2015-2018 Rex
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Crust
A flexible Swift framework for converting classes and structs to and from JSON with support for storage solutions such as Realm.
Features 🎸
Requirements
iOS 10.0+ Swift 5.0+
v 0.12.0 for iOS 8 see
swift-3
tag (v 0.6.0..<0.7.0) for Swift 3 seeswift-4.0
tag for Swift 4.0 seeswift-4.2
tag for Swift 4.2Installation
CocoaPods
Swift Package Manager (SPM)
Structs and Classes
Can map to/from classes or structs
If you have no need for storage, which will generaly be the case for structs, use
AnyMappable
.Separation of Concerns
By design Crust is built with separation of concerns in mind. It makes no assumptions about how many ways a user would like to map to and from JSON and how many various ways the user would like to store their models.
Crust has 2 basic protocols:
Mapping
- How to map JSON to and from a particular model - (model is set by theassociatedtype MappedObject
if mapping to an sequence of objects setassociatedtype SequenceKind
). - May include primary key(s) and nested mapping(s).PersistanceAdapter
- How to store and retrieve model objects used for mapping from a backing store (e.g. Core Data, Realm, etc.).And 2 additional protocols when no storage
PersistanceAdapter
is required:AnyMappable
- Inherited by the model (class or struct) to be mapped to and from JSON.AnyMapping
- AMapping
that does not require anPersistanceAdapter
.There are no limitations on the number of various
Mapping
s andPersistanceAdapter
s one may create per model for different use cases.JSONValue for type safe JSON
Crust relies on JSONValue for it’s JSON encoding and decoding mechanism. It offers many benefits including type safety, subscripting, and extensibility through protocols.
How To Map
Create a set of
MappingKey
s that define the key paths from the JSON payload to your model.Create your mappings for your model using
Mapping
if with storage orAnyMapping
if without storage.With storage (assume
CoreDataAdapter
conforms toPersistanceAdapter
)Without storage
Create your Crust Mapper.
Use the mapper to convert to and from
JSONValue
objectsNOTE:
JSONValue
can be converted back to anAnyObject
variation of json viajson.values()
and toNSData
viatry! json.encode()
.Nested Mappings
Crust supports nested mappings for nested models E.g. from above
Binding and Collections
Binding
provides specialized directives when mapping collections. Use the.collectionMapping
case to inform the mapper of these directives. They includeElement
s of the collection being mapped followEquatable
.Element
s do not followEquatable
then uniquing is ignored unlessUniquingFunctions
are explicitly provided and the mapping functionmap(toCollection field:, using binding:, uniquing:)
is used.This table provides some examples of how “null” json values are mapped depending on the type of Collection being mapped to and given the value of
nullable
and whether values or “null” are present in the JSON payload.By default using
.mapping
will(insert: .replace(delete: nil), unique: true, nullable: true)
.Usage:
Look in ./Mapper/MappingProtocols.swift for more.
Mapping Payload
Every
mapping
passes through aPayload: MappingPayload<T>
which must be included during the mapping. Thepayload
includes error information that is propagated back from the mapping to the caller and contextual information about the json and object being mapped to/from.To include the payload during mapping include it as a tuple.
Custom Transformations
To create a simple custom transformation (such as to basic value types) implement the
Transform
protocoland use it like any other
Mapping
.Different Mappings for Same Model
Multiple
Mapping
s are allowed for the same model.Just use two different mappings.
Persistance Adapter
Follow the
PersistanceAdapter
protocol to store data into Core Data, Realm, etc.The object conforming to
PersistanceAdapter
must include twoassociatedtype
s:BaseType
- the top level class for this storage systems model objects.NSManagedObject
.RLMObject
.Object
.ResultsType: Collection
- Used for object lookups. Should be set to a collection ofBaseType
s.The
Mapping
must then set it’sassociatedtype AdapterKind = <Your Adapter>
to use it during mapping.Realm
There are tests included in
./RealmCrustTests
that include examples of how to use Crust with realm-cocoa (Obj-C).If you wish to use Crust with RealmSwift check out this (slightly outdated) repo for examples. https://github.com/rexmas/RealmCrust
Contributing
Pull requests are welcome!
swift test --generate-linuxmain
License
The MIT License (MIT)
Copyright (c) 2015-2018 Rex
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.