Declarative Networking with Combine
Tags: ios, swift, combine, reactive
Published: 2019-12-17 09:13:16

---
title: Declarative Networking with Combine
published: true
description: Declarative Networking with Combine
tags: ios, swift, combine, reactive
cover_image: https://thepracticaldev.s3.amazonaws.com/i/gqbtxx0op2js9aubunqg.png
---

[*Originally posted on my personal blog at riteshhh.com*](https://riteshhh.com/combine/declarative-networking-with-combine/) 🙇 🙈

In this post, we will explore how we can form a declarative networking layer using Combine for iOS applications. The idea here is not to re-create `URLSession.DataTaskPublisher` operator but rather to understand and take advantage of the existing declarative nature of Combine (and its operators) to manage success and error values declaratively. I know I have already over used the term `declarative`, so please bear with me 😅

The Combine framework has a built-in operator named `DataTaskPublisher` along with a convenience method on `URLSession` which can be used to hit a network request i.e. 
```swift
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher
```
Though before we go ahead and parse server's response we need to validate it so that we can segregate the error values from the success values. Once we have established this clear separation between the two paths, we can then parse them correctly and pass on the values in their respective streams. Also, the Combine framework doesn't know how to understand custom error cases that a server might throw (`4xx/5xx`) or some hidden key/value pair in headers. So we need to handle two things to move forward,

1. How to add validation rules to segregate success and error values?
2. How to map the server's error response to a custom type and forward its value via error stream only?

Let's take a look at the final API and the various reactive operators used to achieve it, 

```swift
extension URLSession {
    func dataTaskPublisher<Output: Decodable>(for request: URLRequest) -> AnyPublisher<Output, Error> {
        return self
            .dataTaskPublisher(for: request)
            .validateStatusCode({ (200..<300).contains($0) })
            .mapJsonError(to: ApiErrorResponse.self, decoder: JSONDecoder())
            .mapJsonValue(to: Output.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}
```
<br/>

Now let's break it line by line to understand what we are trying to achieve here:

### 1. func dataTaskPublisher<>(...)
```swift
func dataTaskPublisher<Output: Decodable>(for request: URLRequest) -> AnyPublisher<Output, Error> { ... }
```
First thing first, the function name and its arguments is similar to Combine's extension function i.e. `func dataTaskPublisher(for request: URLRequest)`. Though the output type is a bit different i.e our function returns a generic output type `<Output: Decodable>` which is a type-safe version of what we are expecting from the server. Since `Output ` is not the type itself but represents any type which conforms to `Decodable`, it makes it quite flexible.


### 2. dataTaskPublisher(for :)
```swift
.dataTaskPublisher(for: request)
```
We are building on top of the existing operator by Combine i.e. `dataTaskPublisher(for: request)` hence we are calling it the first thing which returns a stream of `(Data, URLResponse)` on success and `URLError` on error. Under the hood, it takes care of making a data task and uses it to hit a network request.

### 3. validateStatusCode(...)
```swift
.validateStatusCode({ (200..<300).contains($0) })
```
After we have recieved a response from the server (i.e. json/xml or maybe error), we would like to validate the upstream value into separate success and error streams based on our business logic. Here's the internal implementation on the validation process,

```swift
typealias DataTaskResult = (data: Data, response: URLResponse)

extension Publisher where Output == DataTaskResult {
    func validateStatusCode(_ isValid: @escaping (Int) -> Bool) -> AnyPublisher<Output, ValidationError> {
        return validateResponse { (data, response) in
            let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
            return isValid(statusCode)
        }
    }

    func validateResponse(_ isValid: @escaping (DataTaskResult) -> Bool) -> AnyPublisher<Output, ValidationError> {
        return self
            .mapError { .error($0) }
            .flatMap { (result) -> AnyPublisher<DataTaskResult, ValidationError> in
                let (data, _) = result
                if isValid(result) {
                    return Just(result)
                        .setFailureType(to: ValidationError.self)
                        .eraseToAnyPublisher()
                } else {
                    return Fail(outputType: Output.self, failure: .jsonError(data))
                        .eraseToAnyPublisher()
                }}
            .eraseToAnyPublisher()
    }
}
```

`validateResponse` accepts a closure that provides the status code of the response and returns a boolean based on the business logic. In this case, if the status code is between `200..<300` then it's a success otherwise it's an error. We could have our custom logic as well where we might want to use some other things like header values or to check whether the data is empty or not. One could also write a generic version i.e. `func validateResponse(_ isValid: (DataTaskResult) -> Bool)` for more customisations. 

- A thing to note here is that it only checks for validation if the upstream sends a success value. In case of a default/generic failure, it simply bypasses the validation logic and passes the error value via `.mapError { .error($0) }` to the next operator. 
  
- Another thing to note here is that we don't modify the incoming success values but only lift the incoming error value from `URLError` to `ValidationError` which looks like below,

```swift
enum ValidationError: Error {
    case error(Error)
    case jsonError(Data)
}
```

### 4. mapJsonError(to :)
```swift
.mapJsonError(to: APIErrorResponse.self)
```

Now at this point, we have lifted the incoming error value to `ValidationError` so that we can parse JSON error response into a custom type. Here's the inner implementation,

```swift
extension Publisher where Failure == ValidationError {
    func mapJsonError<E: Error & Decodable>(to errorType: E.Type, decoder: JSONDecoder) -> AnyPublisher<Output, Error> {
        return self
            .catch { (error: ValidationError) -> AnyPublisher<Output, Error> in
                switch error {
                case .error(let e):
                    return Fail(outputType: Output.self, failure: e)
                        .eraseToAnyPublisher()
                case .jsonError(let d):
                    return Just(d)
                        .decode(type: E.self, decoder: decoder)
                        .flatMap { Fail(outputType: Output.self, failure: $0) }
                        .eraseToAnyPublisher() } }
            .eraseToAnyPublisher()
    }
}
```

It takes a custom error type which we want to model against the server's error response/message. A thing to note here is that it only works for an error path i.e. in case of success Combine framework won't even call this operator which is what we desire as well.

### 5. mapJsonValue(to :)
```swift
mapJsonValue(to: Output.self)
```

So far we have handled the validation of the server's response and formation of custom error object. Now we can safely parse the success JSON value into a custom type that can be passed onto the UI layer (or data layer depending on your architecture but we are not here to discuss that 😉). 

```swift
extension Publisher where Output == DataTaskResult {
    func mapJsonValue<Output: Decodable>(to outputType: Output.Type, decoder: JSONDecoder) -> AnyPublisher<Output, Error> {
        return self
            .map(\.data)
            .decode(type: outputType, decoder: decoder)
            .eraseToAnyPublisher()
    }}

```

### 6. eraseToAnyPublisher()

```swift
eraseToAnyPublisher()
```
Last but not least, we have used `eraseToAnyPublisher()` so that we can hide the implementation detail and provide a simple API for consumption.

</br>

Thus by hooking all the pieces together, we get this nice declarative API which can be used to parse success and error values declaratively along with some validation 🤩 Here's the summary of the stream value types as they move along the reactive chain,

```swift
extension URLSession {
    func dataTaskPublisher<Output: Decodable>(for request: URLRequest) -> AnyPublisher<Output, Error> {
        return self
            .dataTaskPublisher(for:) // (DataTaskResult, URLError)
            .validateStatusCode(...) // (DataTaskResult, ValidationError)
            .mapJsonError(to:)       // (DataTaskResult, ResponseError)
            .mapJsonValue(to:)       // (ResponseValue, ResponseError)
            .eraseToAnyPublisher()   // (ResponseValue, ResponseError)
    }
}
```

</br>

Thanks for reading  🙌  You can reach out to me at [@_riteshhh](https://twitter.com/_riteshhh) on twitter 😄

*(Some images are copyright Icons8 LLC © 2019)*