Skip to content

Commit

Permalink
Merge pull request #3 from AckeeCZ/marshal_mapping
Browse files Browse the repository at this point in the history
Marshal mapping
  • Loading branch information
janmisar authored Apr 12, 2017
2 parents 0673473 + 8fefcab commit 4abfee1
Show file tree
Hide file tree
Showing 43 changed files with 4,718 additions and 2,785 deletions.
6 changes: 6 additions & 0 deletions ACKReactiveExtensions.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,11 @@ Pod::Spec.new do |s|
realm.dependency 'RealmSwift', '~> 2.1'
realm.source_files = 'ACKReactiveExtensions/Realm/**/*'
end

s.subspec 'Marshal' do |marshal|
marshal.dependency 'ACKReactiveExtensions/Core'
marshal.dependency 'Marshal', '~> 1.2'
marshal.source_files = 'ACKReactiveExtensions/Marshal/**/*'
end

end
124 changes: 124 additions & 0 deletions ACKReactiveExtensions/Marshal/MarshalMapping.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//
// MarshalMapping.swift
// ACKReactiveExtensions
//
// Created by Jakub Olejník on 06/04/2017.
// Ackee
//

import Result
import Marshal
import ReactiveSwift

/**
* Protocol that allows creation of custom Marshal errors
*/
public protocol MarshalErrorCreatable: Error {

/**
* Create error containing passed `MarshalError`
*
* - parameter marshalError: `MarshalError` which should be wrapped
*/
static func createMarshalError(_ marshalError: MarshalError) -> Self
}

extension MarshalError: MarshalErrorCreatable {
public static func createMarshalError(_ marshalError: MarshalError) -> MarshalError {
return marshalError
}
}

extension SignalProtocol where Value == Any, Error: MarshalErrorCreatable {

/**
* Map value as `Unmarshaling` object
*
* - parameter forKey: If your objects are contained within dictionary pass the key here
*/
public func mapResponse<Model>(forKey key: KeyType? = nil) -> Signal<Model, Error> where Model: Unmarshaling {
return attemptMap { json in
Result {
guard let marshaledJSON = json as? MarshaledObject
else {
throw MarshalError.typeMismatch(expected: MarshaledObject.self, actual: type(of: json))
}
if let key = key {
return try marshaledJSON.value(for: key)
} else {
return try Model.init(object: marshaledJSON)
}
}
.mapError { Error.createMarshalError($0) }
}
}

/**
* Map value as `Unmarshaling` object
*
* - parameter forKey: If your objects are contained within dictionary pass the key here
*/
public func mapResponse<Model>(forKey key: KeyType? = nil) -> Signal<[Model], Error> where Model: Unmarshaling {
return attemptMap { json in
Result {
if let key = key, let marshaledJSON = json as? MarshaledObject {
return try marshaledJSON.value(for: key)
}
else if let marshaledArray = json as? [MarshaledObject] {
return try marshaledArray.map(Model.init)
}
else {
throw MarshalError.typeMismatch(expected: MarshaledObject.self, actual: type(of: json))
}
}
.mapError { Error.createMarshalError($0) }
}
}

/**
* Map value as `ValueType`
*
* - parameter forKey: If your objects are contained within dictionary pass the key here
*/
public func mapResponse<Model>(forKey key: KeyType) -> Signal<Model, Error> where Model: ValueType {
return attemptMap { json in
Result {
guard let marshaledJSON = json as? MarshaledObject
else {
throw MarshalError.typeMismatch(expected: MarshaledObject.self, actual: type(of: json))
}
return try marshaledJSON.value(for: key)
}
.mapError { Error.createMarshalError($0) }
}
}
}

extension SignalProducerProtocol where Value == Any, Error: MarshalErrorCreatable {
/**
* Map value as `Unmarshaling` object
*
* - parameter forKey: If your objects are contained within dictionary pass the key here
*/
public func mapResponse<Model>(forKey key: KeyType? = nil) -> SignalProducer<Model, Error> where Model: Unmarshaling {
return lift { $0.mapResponse(forKey: key) }
}

/**
* Map value as `Unmarshaling` object
*
* - parameter forKey: If your objects are contained within dictionary pass the key here
*/
public func mapResponse<Model>(forKey key: KeyType? = nil) -> SignalProducer<[Model], Error> where Model: Unmarshaling {
return lift { $0.mapResponse(forKey: key) }
}

/**
* Map value as `ValueType`
*
* - parameter forKey: If your objects are contained within dictionary pass the key here
*/
public func mapResponse<Model>(forKey key: KeyType) -> SignalProducer<Model, Error> where Model: ValueType {
return lift { $0.mapResponse(forKey: key) }
}
}
99 changes: 99 additions & 0 deletions Docs/Marshal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Marshal extensions

ACKReactiveExtensions contain extensions that allow you to simply call correct Marshal mapping function. Extensions are written for `Signal`s and `SignalProducer`s based on their value and error type.

You can use default `MarshalError` which is supported out of box or any custom error you like. Your error just needs to conform to `MarshalErrorCreatable` protocol because in case of a mapping failure we need to be capable of creating your error with appropriate message.

## Sample usage

We generally assume that you have a `Signal` or `SignalProducer` which produces your data and we were like _Okay and how we can get rid of all that boring boilerplate code that takes our dictionary/array and converts them into my model objects_. Out solution is extension which wraps all that stuff into single generic method call.

API calls can return a big variety of errors so the `MarshalError` might be a problem and this is where our custom error protocol comes to rescue. You can use any Swift structure as your error type, all you have to do is conform `MarshalErrorCreatable` protocol because we like to inform you in case that something goes wrong with the mapping. We like using enums for our error types so I'm gonna use it in this example.

```swift
enum MyError: Error {
case request
case mapping(MarshalError)
}
```

I need to conform it to `MarshalErrorCreatable`:

```swift
extension MyError: MarshalErrorCreatable {
func createMarshalError(_ marshalError: MarshalError) -> MyError {
return .mapping(marshalError)
}
}
```

That was simple right?

You also need some model object so you have something you can map to right:

```swift
struct Car {
let manufacturer: String
let model: String
}
```

Now in your API calls you just call `mapResponse()` method and you get what you want.

```swift
func fetchCars() -> SignalProducer<[Car], MyError> {
let apiCall: SignalProducer<Any, MyError> = ... // make your api call
return apiCall.mapResponse()
}
```

That's nicer than using various `map`s and `flatMap`s in your every single call, isn't it?

## Advanced usage

### Use root key

In case your data aren't always root objects of your API response you can use `key` parameter of `mapResponse()` method.

```swift
func fetchCars() -> SignalProducer<[Car], MyError> {
let apiCall: SignalProducer<Any, MyError> = ... // make your api call
return apiCall.mapResponse(for: "data")
}
```

### Object transformations and ambiguity

In some cases you might need to perform some other transformations with you objects. This might occasionally become little tricky because the compiler needs to know which object should be mapped. In the previous sample the final type of expression was determined by the return type of `fetchCars()` function, but in some cases it isn't as straightforward.

Just assume the you are just interested in fetching just manufacturers of all cars:
```swift
func fetchManufacturers() -> SignalProducer<[String], MyError> {
let apiCall: SignalProducer<Any, MyError> = ... // make your api call just like you did before
return apiCall
.mapResponse() // ambiguous use of mapResponse()
.map { $0.map { $0.manufacturer } }
}
```

Now you end up with ambiguity. How so? It's simple, now the compiler doesn't know that you want to map `[Car]` and the solution is simple, you just tell him:

```swift
func fetchManufacturers() -> SignalProducer<[String], MyError> {
let apiCall: SignalProducer<Any, MyError> = ... // make your api call just like you did before
return apiCall
.mapResponse()
.map { (cars: [Car]) in
return cars.map { $0.manufacturer }
}
}
```

Now you're all set and ready. The same issue arises if you don't return you `SignalProducer` directly but save him into local variable. The solution is the same:
```swift
func fetchCars() -> SignalProducer<[Car], MyError> {
let apiCall: SignalProducer<Any, MyError> = ... // make your api call
let carsProducer: SignalProducer<[Car], MyError> = apiCall.mapResponse()
return carsProducer
}
```
4 changes: 4 additions & 0 deletions Example/ACKReactiveExtensions.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
693402381D1296C3004F1FFB /* UIKitExtensionsThreadingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693402371D1296C3004F1FFB /* UIKitExtensionsThreadingSpec.swift */; };
69FDE6FD1E968255004FD0D7 /* MarshalMappingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FDE6FC1E968255004FD0D7 /* MarshalMappingTests.swift */; };
69FDE6F71E965C56004FD0D7 /* ArgoMappingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FDE6F51E965C32004FD0D7 /* ArgoMappingTests.swift */; };
69FDE6F91E966111004FD0D7 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FDE6F81E966111004FD0D7 /* TestHelpers.swift */; };
93633F05A07E7E137AB05065 /* Pods_ACKReactiveExtensions_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 482304D33E40D8C0EF1F687B /* Pods_ACKReactiveExtensions_Example.framework */; };
Expand Down Expand Up @@ -46,6 +47,7 @@
607FACE51AFB9204008FA782 /* ACKReactiveExtensions_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ACKReactiveExtensions_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
693402371D1296C3004F1FFB /* UIKitExtensionsThreadingSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitExtensionsThreadingSpec.swift; sourceTree = "<group>"; };
69FDE6FC1E968255004FD0D7 /* MarshalMappingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarshalMappingTests.swift; sourceTree = "<group>"; };
69FDE6F51E965C32004FD0D7 /* ArgoMappingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArgoMappingTests.swift; sourceTree = "<group>"; };
69FDE6F81E966111004FD0D7 /* TestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
AA849E88A474C832B5FE7224 /* Pods_ACKReactiveExtensions_Example_ACKReactiveExtensions_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ACKReactiveExtensions_Example_ACKReactiveExtensions_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -133,6 +135,7 @@
607FACE91AFB9204008FA782 /* Supporting Files */,
693402371D1296C3004F1FFB /* UIKitExtensionsThreadingSpec.swift */,
ACF37BCF1E26CED900FE04FF /* RealmTest.swift */,
69FDE6FC1E968255004FD0D7 /* MarshalMappingTests.swift */,
69FDE6F51E965C32004FD0D7 /* ArgoMappingTests.swift */,
69FDE6F81E966111004FD0D7 /* TestHelpers.swift */,
);
Expand Down Expand Up @@ -383,6 +386,7 @@
files = (
693402381D1296C3004F1FFB /* UIKitExtensionsThreadingSpec.swift in Sources */,
ACF37BD01E26CED900FE04FF /* RealmTest.swift in Sources */,
69FDE6FD1E968255004FD0D7 /* MarshalMappingTests.swift in Sources */,
69FDE6F71E965C56004FD0D7 /* ArgoMappingTests.swift in Sources */,
69FDE6F91E966111004FD0D7 /* TestHelpers.swift in Sources */,
);
Expand Down
1 change: 1 addition & 0 deletions Example/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ target 'ACKReactiveExtensions_Example' do
pod 'ACKReactiveExtensions/SDWebImage', :path => '../'
pod 'ACKReactiveExtensions/WebKit', :path => '../'
pod 'ACKReactiveExtensions/Realm', :path => '../'
pod 'ACKReactiveExtensions/Marshal', :path => '../'

target 'ACKReactiveExtensions_Tests' do
pod 'Quick', '~> 0.10'
Expand Down
15 changes: 11 additions & 4 deletions Example/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ PODS:
- ReactiveCocoa (~> 5.0)
- ACKReactiveExtensions/Core (2.3.0):
- ReactiveCocoa (~> 5.0)
- ACKReactiveExtensions/Marshal (2.3.0):
- ACKReactiveExtensions/Core
- Marshal (~> 1.2)
- ReactiveCocoa (~> 5.0)
- ACKReactiveExtensions/Reachability (2.3.0):
- ACKReactiveExtensions/Core
- Reachability
Expand All @@ -29,6 +33,7 @@ PODS:
- Argo (4.1.2):
- Runes (>= 4.0.0)
- Curry (3.0.0)
- Marshal (1.2.4)
- Nimble (5.1.1)
- Quick (0.10.0)
- Reachability (3.2)
Expand All @@ -50,23 +55,25 @@ PODS:
DEPENDENCIES:
- ACKReactiveExtensions (from `../`)
- ACKReactiveExtensions/Argo (from `../`)
- ACKReactiveExtensions/Marshal (from `../`)
- ACKReactiveExtensions/Reachability (from `../`)
- ACKReactiveExtensions/Realm (from `../`)
- ACKReactiveExtensions/SDWebImage (from `../`)
- ACKReactiveExtensions/WebKit (from `../`)
- Curry
- Curry (~> 3.0)
- Nimble (~> 5.0)
- Quick (~> 0.10)
- Result
- Result (~> 3.1)

EXTERNAL SOURCES:
ACKReactiveExtensions:
:path: "../"

SPEC CHECKSUMS:
ACKReactiveExtensions: f1116fa43f95f1271dc059b9b2dfb38eb5a5ea48
ACKReactiveExtensions: f35f2afcd9e5b324d92883c94b4c3d724118781f
Argo: 5db06502fc1222d83011f574243089dab25202ff
Curry: eb3d2c75aec678b3cc7fb2729d2ad6dd121531d1
Marshal: 8e04e6624e506921db7143b0bfd83caee03f32d6
Nimble: 415e3aa3267e7bc2c96b05fa814ddea7bb686a29
Quick: 5d290df1c69d5ee2f0729956dcf0fd9a30447eaa
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
Expand All @@ -78,6 +85,6 @@ SPEC CHECKSUMS:
Runes: ff20f163b478ac2c0e18158d6086fd5b1997f983
SDWebImage: 76a6348bdc74eb5a55dd08a091ef298e56b55e41

PODFILE CHECKSUM: a0398120d2fe073a22a4a96d9256f896190a9cb5
PODFILE CHECKSUM: 1d0475767d76bb45cefcaceddd19be37d22fedc7

COCOAPODS: 1.2.0
12 changes: 12 additions & 0 deletions Example/Pods/Local Podspecs/ACKReactiveExtensions.podspec.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4abfee1

Please sign in to comment.