KeyPathMapper 2.0.1

KeyPathMapper 2.0.1

Maintained by Evghenii Nicolaev.



KeyPathMapper

Easy way of mapping models.

Build Status platforms pod Swift Package Manager compatible

Why map?

Taken form dozer framework

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 in RootType. (ex: Person.name) For WritableMapAddress 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 on InType.

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.