Skip to content

Commit

Permalink
Merge pull request #2 from AckeeCZ/argo_mapping
Browse files Browse the repository at this point in the history
Argo mapping
  • Loading branch information
janmisar authored Apr 7, 2017
2 parents d8a3dfe + ccc8653 commit 0673473
Show file tree
Hide file tree
Showing 27 changed files with 3,502 additions and 2,612 deletions.
2 changes: 1 addition & 1 deletion ACKReactiveExtensions.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Pod::Spec.new do |s|
end

s.subspec 'Argo' do |argo|
argo.dependency 'Argo', '~> 4.0'
argo.dependency 'Argo', '>= 4.1.2', '< 5.0'
argo.source_files = 'ACKReactiveExtensions/Argo/**/*'
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Argo
* - parameter object: Source object to decode
* - returns: SignalProducer that sends decoded object
*/
@available(*, deprecated, message: "Use extension mapResponseArgo() on SignalProducer<Any,DecodeErrorCreatable>")
public func rac_decode < T: Decodable where T == T.DecodedType > (object: AnyObject) -> SignalProducer<T, DecodeError> {
return SignalProducer { sink, disposable in

Expand All @@ -36,6 +37,7 @@ public func rac_decode < T: Decodable where T == T.DecodedType > (object: AnyObj
* - parameter object: Source object to decode
* - returns: SignalProducer that sends decoded array
*/
@available(*, deprecated, message: "Use extension mapResponseArgo() on SignalProducer<Any,DecodeErrorCreatable>")
public func rac_decode < T: Decodable where T == T.DecodedType > (object: AnyObject) -> SignalProducer<[T], DecodeError> {
return SignalProducer { sink, disposable in

Expand All @@ -58,6 +60,7 @@ public func rac_decode < T: Decodable where T == T.DecodedType > (object: AnyObj
* - parameter object: Source object to decode
* - returns: SignalProducer that sends decoded array
*/
@available(*, deprecated, message: "Use extension mapResponseArgo() on SignalProducer<Any,DecodeErrorCreatable>")
public func rac_decodeWithRootKey < T: Decodable where T == T.DecodedType > (rootKey: String, object: AnyObject) -> SignalProducer<[T], DecodeError> {
return SignalProducer { sink, disposable in

Expand Down Expand Up @@ -85,6 +88,7 @@ public func rac_decodeWithRootKey < T: Decodable where T == T.DecodedType > (roo
* - parameter object: Source object to decode
* - returns: SignalProducer that sends decoded object
*/
@available(*, deprecated, message: "Use extension mapResponseArgo() on SignalProducer<Any,DecodeErrorCreatable>")
public func rac_decodeWithRootKey < T: Decodable where T == T.DecodedType > (rootKey: String, object: AnyObject) -> SignalProducer<T, DecodeError> {
return SignalProducer { sink, disposable in

Expand Down Expand Up @@ -112,6 +116,7 @@ public func rac_decodeWithRootKey < T: Decodable where T == T.DecodedType > (roo
* - parameter object: Source object to decode
* - returns: SignalProducer that sends decoded array one by one
*/
@available(*, deprecated, message: "Use extension mapResponseArgo() on SignalProducer<Any,DecodeErrorCreatable>")
public func rac_decodeByOne < T: Decodable where T == T.DecodedType > (object: AnyObject) -> SignalProducer<T, DecodeError> {
return SignalProducer { sink, disposable in

Expand Down
96 changes: 96 additions & 0 deletions ACKReactiveExtensions/Argo/ArgoMapping.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// ArgoMapping.swift
// ACKReactiveExtensions
//
// Created by Jakub Olejník on 06/04/2017.
// Ackee
//

import Argo
import Result
import ReactiveSwift

/**
* Protocol that allows creation of custom Decode errors
*/
public protocol DecodeErrorCreatable: Error {

/**
* Create error containing passed `DecodeError`
*
* - parameter decodeError: `DecodeError` which should be wrapped
*/
static func createDecodeError(_ decodeError: DecodeError) -> Self
}

extension DecodeError: DecodeErrorCreatable {
public static func createDecodeError(_ decodeError: DecodeError) -> DecodeError {
return decodeError
}
}

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

/**
* Map value as `Decodable` object
*
* - parameter key: If your objects are contained within dictionary pass the key here
*/
public func mapResponseArgo<ResultType: Decodable>(for key: String? = nil) -> Signal<ResultType, Error> where ResultType.DecodedType == ResultType {
return attemptMap { data in
let decoded: Decoded<ResultType> = key.map {
let dict = data as? [String: Any] ?? [:]
return decode(dict, rootKey: $0)
} ?? decode(data)

switch decoded {
case .success(let box):
return Result.success(box)
case .failure(let error):
return Result.failure(Error.createDecodeError(error))
}
}
}

/**
* Map values as `Decodable` objects
*
* - parameter key: If your objects are contained within dictionary pass the key here
*/
public func mapResponseArgo<ResultType: Decodable>(for key: String? = nil) -> Signal<[ResultType], Error> where ResultType.DecodedType == ResultType {
return attemptMap { data in
let decoded: Decoded<[ResultType]> = key.map {
let dict = data as? [String: Any] ?? [:]
return decode(dict, rootKey: $0)
} ?? decode(data)

switch decoded {
case .success(let box):
return Result.success(box)
case .failure(let error):
return Result.failure(Error.createDecodeError(error))
}
}
}
}

extension SignalProducerProtocol where Value == Any, Error: DecodeErrorCreatable {

/**
* Map value as `Decodable` object
*
* - parameter key: If your objects are contained within dictionary pass the key here
*/
public func mapResponseArgo<ResultType: Decodable>(for key: String? = nil) -> SignalProducer<ResultType, Error> where ResultType.DecodedType == ResultType {
return lift { $0.mapResponseArgo(for: key) }
}

/**
* Map values as `Decodable` objects
*
* - parameter key: If your objects are contained within dictionary pass the key here
*/
public func mapResponseArgo<ResultType: Decodable>(for key: String? = nil) -> SignalProducer<[ResultType], Error> where ResultType.DecodedType == ResultType {
return lift { $0.mapResponseArgo(for: key) }
}
}
99 changes: 99 additions & 0 deletions Docs/Argo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Argo extensions

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

You can use default `DecodeError` from [Argo](https://github.com/thoughtbot/Argo) which is supported out of box or any custom error you like. Your error just needs to conform to `DecodeErrorCreatable` 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 `DecodeError` 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 `DecodeErrorCreatable` protocol because we like to inform you in case that something goes wrong with tha 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(DecodeError)
}
```

I need to conform it to `DecodeErrorCreatable`:

```swift
extension MyError: DecodeErrorCreatable {
func createDecodeError(_ decodeError: DecodeError) -> MyError {
return .mapping(decodeError)
}
}
```

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 `mapResponseArgo()` method and you get what you want.

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

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 `mapResponseArgo()` method.

```swift
func fetchCars() -> SignalProducer<[Car], MyError> {
let apiCall: SignalProducer<Any, MyError> = ... // make your api call
return apiCall.mapResponseArgo(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
.mapResponseArgo() // ambiguous use of mapResponseArgo()
.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
.mapResponseArgo()
.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.mapResponseArgo()
return carsProducer
}
```
8 changes: 8 additions & 0 deletions Example/ACKReactiveExtensions.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
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 */; };
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 */; };
ACF37BD01E26CED900FE04FF /* RealmTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF37BCF1E26CED900FE04FF /* RealmTest.swift */; };
E7AF6B155A6030F53856FC10 /* Pods_ACKReactiveExtensions_Example_ACKReactiveExtensions_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA849E88A474C832B5FE7224 /* Pods_ACKReactiveExtensions_Example_ACKReactiveExtensions_Tests.framework */; };
Expand Down Expand Up @@ -44,6 +46,8 @@
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>"; };
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; };
ACF37BCF1E26CED900FE04FF /* RealmTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmTest.swift; sourceTree = "<group>"; };
B3CB8296538D6AF90E03A00A /* Pods-ACKReactiveExtensions_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ACKReactiveExtensions_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ACKReactiveExtensions_Example/Pods-ACKReactiveExtensions_Example.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -129,6 +133,8 @@
607FACE91AFB9204008FA782 /* Supporting Files */,
693402371D1296C3004F1FFB /* UIKitExtensionsThreadingSpec.swift */,
ACF37BCF1E26CED900FE04FF /* RealmTest.swift */,
69FDE6F51E965C32004FD0D7 /* ArgoMappingTests.swift */,
69FDE6F81E966111004FD0D7 /* TestHelpers.swift */,
);
path = Tests;
sourceTree = "<group>";
Expand Down Expand Up @@ -377,6 +383,8 @@
files = (
693402381D1296C3004F1FFB /* UIKitExtensionsThreadingSpec.swift in Sources */,
ACF37BD01E26CED900FE04FF /* RealmTest.swift in Sources */,
69FDE6F71E965C56004FD0D7 /* ArgoMappingTests.swift in Sources */,
69FDE6F91E966111004FD0D7 /* TestHelpers.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
3 changes: 2 additions & 1 deletion Example/Podfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use_frameworks!

target 'ACKReactiveExtensions_Example' do
pod 'Result'
pod 'Result', '~> 3.1'
pod 'ACKReactiveExtensions', :path => '../'
pod 'ACKReactiveExtensions/Argo', :path => '../'
pod 'ACKReactiveExtensions/Reachability', :path => '../'
Expand All @@ -12,5 +12,6 @@ target 'ACKReactiveExtensions_Example' do
target 'ACKReactiveExtensions_Tests' do
pod 'Quick', '~> 0.10'
pod 'Nimble', '~> 5.0'
pod 'Curry', '~> 3.0'
end
end
9 changes: 6 additions & 3 deletions Example/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ PODS:
- ACKReactiveExtensions/UIKit (= 2.3.0)
- ReactiveCocoa (~> 5.0)
- ACKReactiveExtensions/Argo (2.3.0):
- Argo (~> 4.0)
- Argo (< 5.0, >= 4.1.2)
- ReactiveCocoa (~> 5.0)
- ACKReactiveExtensions/Core (2.3.0):
- ReactiveCocoa (~> 5.0)
Expand All @@ -28,6 +28,7 @@ PODS:
- ReactiveCocoa (~> 5.0)
- Argo (4.1.2):
- Runes (>= 4.0.0)
- Curry (3.0.0)
- Nimble (5.1.1)
- Quick (0.10.0)
- Reachability (3.2)
Expand All @@ -53,6 +54,7 @@ DEPENDENCIES:
- ACKReactiveExtensions/Realm (from `../`)
- ACKReactiveExtensions/SDWebImage (from `../`)
- ACKReactiveExtensions/WebKit (from `../`)
- Curry
- Nimble (~> 5.0)
- Quick (~> 0.10)
- Result
Expand All @@ -62,8 +64,9 @@ EXTERNAL SOURCES:
:path: "../"

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

PODFILE CHECKSUM: 6c1b7d05f93f381c4bd7338de6c376b1606452db
PODFILE CHECKSUM: a0398120d2fe073a22a4a96d9256f896190a9cb5

COCOAPODS: 1.2.0
22 changes: 22 additions & 0 deletions Example/Pods/Curry/LICENSE

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

Loading

0 comments on commit 0673473

Please sign in to comment.