-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from AckeeCZ/argo_mapping
Argo mapping
- Loading branch information
Showing
27 changed files
with
3,502 additions
and
2,612 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.