-
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 #3 from AckeeCZ/marshal_mapping
Marshal mapping
- Loading branch information
Showing
43 changed files
with
4,718 additions
and
2,785 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
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) } | ||
} | ||
} |
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 @@ | ||
# 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 | ||
} | ||
``` |
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
12 changes: 12 additions & 0 deletions
12
Example/Pods/Local Podspecs/ACKReactiveExtensions.podspec.json
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.