Easy way of mapping models.
Why map?
A mapping framework is useful in a layered architecture where you are creating layers of abstraction by encapsulating changes to particular data objects vs. propagating these objects to other layers (i.e. external service data objects, domain objects, data transfer objects, internal service data objects). Mapping between data objects has traditionally been addressed by hand coding value object assemblers (or converters) that copy data between the objects. Most programmers will develop some sort of custom mapping framework and spend countless hours and thousands of lines of code mapping to and from their different data object.
What is KeyPathMapper?
KeyPathMapper is a lightweight library oriented to provide convinient way of mapping types. It's using swift KeyPath
feature to identify properties that you want to map.
Benefits of using KeyPathMapper
Think of situations when you don't want to mess network, persistent and domain layers. Declare mappers in your app to enable easy and efficient way of mapping.
- Wrap mapping loggic into an object and don't add any mapping functions to a model.
- Use the power of
KeyPath
feature. That means you can map something like:\Person.friends[0].name
- Minimal amound of code, just specify which keyPath of T1 maps to keyPath of T2.
- Allows to perform additional computation on mapping value.
- Extensible through
Transformers
or extensions on MapAddresses.
Example of use
let mapper = OneWayMapper<PersonDetails, Person>()
mapper += (\PersonDetails.name).writableMapAddress <-> (\Person.firstName).readableMapAddress
mapper.register()
let person = Person(name: “John”)
// Then you can use `person => PersonDetails.self` to map to Person type to PersonDetails
let details = try? (person => PersonDetails.self)
Or you can use it with swift types, like Arrays, Dictionaries
let mapper = OneWayMapper<RequestOptionalResult, Dictionary<String, String>>()
mapper += (\RequestOptionalResult.value).writableMapAddress.flatMap { String($0) } <-> (\Dictionary<String, String>["key"]).writableMapAddress.flatMap { Int($0) }
var request = RequestOptionalResult(value: 0)
let dictionary = ["key": "99"]
try? mapper.update(&request, with: dictionary)
XCTAssertEqual(request.value, 99)
Please check tests for more examples.
Installation
CocoaPods
pod "KeyPathMapper"
Swift Package Manager
Create a file Package.swift
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "YourProject",
dependencies: [
.package(url: "https://github.com/marshallxxx/KeyPathMapper.git", "2.0.0" ..< "3.0.0")
],
targets: [
.target(name: "YourProject", dependencies: ["KeyPathMapper"])
]
)
KeyPathMapper
components
KeyPathMapper
was designed to be extensible and easy to use. There a mainly two base components:
- MapAddresses - represent an address in a type that you want to map.
- Mappers - Mappers are composed from multiple pair combinations of MapAddresses.
More on MapAddress
MapAddress represents a property address in a type, quite much as a KeyPath
. The difference being that you can apply transformations when you read the value from KeyPath
, you can chain multiple transformations in order to get the right type.
MapAddresses are conforming to MapAddressType
:
public protocol MapAddressType {
associatedtype RootType
associatedtype InType
associatedtype OutType
func evaluate(_ type: RootType) throws -> OutType
}
There are 3 associated types:
RootType
- represent the type that you want to map. (ex:Person
)InType
- represent the property type inRootType
. (ex:Person.name
) ForWritableMapAddress
this means the type you can set on this MapAddress.OutType
- represent the value type that you get from MapAddress on read. You can modify this type by applying different transformation on initial value. This is the result of a series of transformations onInType
.
ReadableMapAddress
ReadableMapAddress
allows just reading of the value.
There are two ways of creating of a ReadableMapAddress
:
- Through an init:
ReadableMapAddress(keyPath: \Person.name)
- Or through an extension on
KeyPath
:\Person.name.readableMapAddress
WritableMapAddress
WritableMapAddress
allows read and write into the KeyPath.
There are two ways of creating of a WritableMapAddress
:
- Through an init:
WritableMapAddress(writableKeyPath: \Person.name)
- Or through an extension on
WritableKeyPath
:\Person.name.writableMapAddress
MapAddress Operations
Apply
apply
- you can apply a transformation on a MapAddress to modify OutType
. If you have a really complex logic, you might want to wrap in a separate type which you can use with your MapAddresses. For this you create your type which conforms to Transformer
.
private struct RevertStringTransformer: Transformer {
init(userInfo: Void) {}
func transform(_ input: String) -> String {
return String(input.reversed())
}
}
(\Person.firstName).readableMapAddress.apply(RevertStringTransformer())
Join
join
- Allows you to use a different type property on mapping.
(\Person.firstName).readableMapAddress
.join(keyPath: \Person.lastName, { first, last in
return "\(first) \(last)"
})
Map
map
- allows to map to a different type
(\Person.firstName).readableMapAddress.map { String($0.reversed()) }
OnEmpty
onEmpty
- can be used on MapAddresses with optional OutType
. It will fallback to a default value provided in case of nil
.
(\Request.value).readableMapAddress.onEmpty(fallback: -99)
FlatMap
flatMap
- can be used on MapAddresses with optional OutType
. Maps the value if not nil.
(\Request.value).readableMapAddress.flatMap({ $0 * $0 })
Count Elements
countElements
- can be used with Collection
OutType
MapAddresses. Counts the elements in collection.
(\Request.elements).readableMapAddress.countElements()
Map Elements
mapElements
- can be used with Collection
OutType
MapAddresses. Map elements in collection.
(\Request.elements).readableMapAddress.mapElements { Int($0) ?? 0 }
Filter Elements
filterElements
- can be used with Collection
OutType
MapAddresses. Filter elements in collection.
(\Request.elements).readableMapAddress.filterElements { Int($0).flatMap { $0 > 5 } ?? false }
Reduce Elements
reduceElements
- can be used with Collection
OutType
MapAddresses. Reduce elements in collection to a single value.
(\Request.elements).readableMapAddress.reduceElements("", { a, b in return "\(a)\(b)" })
Sort Elements
sortElements
- can be used with Collection
OutType
MapAddresses. Sort elements in collection.
(\Request.elements).readableMapAddress.sortElements(by: { a, b in return a < b })
Custom operators
Here's the way you can extend MapAddresses with custom transformations.
public extension WritableMapAddress {
func map<ResultType>(_ mapBlock: @escaping (OutType) -> (ResultType)) -> WritableMapAddress<RootType, InType, ResultType> {
return WritableMapAddress<RootType, InType, ResultType>(writableKeyPath: keyPath, transformation: { input, root in
return mapBlock(try self.evaluate(root))
})
}
}
Mappers
Mappers are the types which knows how to map one type to another. KeyPathMapper provides two types of mappers OneWayMapper
and TwoWayMapper
.
let oneWayMapper = OneWayMapper<PersonDetails, Person>()
let twoWayMapper = TwoWayMapper<PersonDetails, Person>()
They allow you to update instance of write type from read type, in case of TwoWayMapper it allows update in both directions. ex:
try? mapper.update(&person, with: personDetails)
try? (personDetails => person) // Update person from personDetails. Applicable for registered mapper.
As well you if type has a default initializer and conforms to DefaultInitializable
you can convert to a type right-away
try? mapper.convert(value)
try? (personDetails => Person.self) // Convert personDetails to a Person instance. Applicable for registered mapper.
To add a mapping to a mapper use:
mapper += (\PersonDetails.title).writableMapAddress <-> (\Person.name).writableMapAddress
<->
operator creates a mapping chain, which then is added to the mapper. Make sure for TwoWayMapper
you use two writableMapAddress
.
OneWayMapper<ToType, FromType>
OneWayMapper
is mapper which maps in a single direction, FromType
to ToType
.
TwoWayMapper<TypeOne, TypeTwo>
TwoWayMapper
is mapper which maps in both directions.
Shared Mappers
There is a concept of registered mappers, which means mapper will be known to whole app. So then you can use =>
operator to make transformations.
let mapper = OneWayMapper<RequestResult, Request>()
mapper += (\RequestResult.value).writableMapAddress <-> (\Request.value).readableMapAddress
mapper.register()
let request = Request(value: "Test")
let result = try? (request => RequestResult.self)
Here are operations you can perform with shared mappers. In case there is no known mapper to handle it will throw an exception: MappingError.noRegisteredMapper
instanceA => instanceB
- update instanceB from instanceA
instanceA => TypeB
- convert instanceA to an instance of TypeB
TypeA =/> TypeB
- remove shared mapper which converts from TypeA to TypeB
KVO & KeyPathMapper
You can observe changes of NSObject. Every time instance of the NSObject is changing keyPath's registered with mapper it will map the new value into the updating instance, so you can have instances synchronized. Check next example:
let mapper = TwoWayMapper<Person, ViewMsodel>()
mapper += (\Person.name).writableMapAddress <-> (\ViewModel.title).writableMapAddress
let person = Person()
let viewModel = ViewModel()
let observation = mapper.observe(person, update: viewModel) { modifiedKeyPath in
XCTAssertEqual(\ViewModel.title, modifiedKeyPath)
}
person.name = "John"
XCTAssertEqual(viewModel.title, "John")
License
This project is licensed under the terms of the MIT license. See the LICENSE file.