Home > Net >  How to Implement API for both Success and Failure Response with Combine Swift
How to Implement API for both Success and Failure Response with Combine Swift

Time:03-05

EDIT: I am trying my level best to make my question simpler, here what I am trying to get a solution for is, I have an API and if my data is valid the API will give the correct response, for which I need to decode with the respective struct in swift.

also if my data is wrong the API will fail and it will produce an error response which is a different struct.

with the use of combine, I only could decode a single struct type.

SO how do I make my decode accept any type? Generics is one way I hoped to solve but here the protocol that I need to implement is an issue I believe restricting me from using generics.

thanks for giving it a try.

// MARK: - ResponseStruct Model

struct ResponseStruct: Codable {
   
}

//MARK: -Protocol

public protocol RequestProtocol{
    associatedtype ResponseOutput 
    func fetchFunction() -> AnyPublisher<ResponseOutput, Error>
}

//Mark: - Implementation

struct RequestStruct: Codable, RequestProtocol {
    typealias ResponseOutput = ResponseStruct

    
    func fetchFunction() -> AnyPublisher<ResponseOutput, Error>  {
        let networkManager = NetworkManager()
        do {
            return try networkManager.apiCall(url: url, method: .post, body: JSONEncoder().encode(self))
                .decode(type: ResponseStruct.self, decoder: JSONDecoder())
                .eraseToAnyPublisher()
        } catch {
            
        }
    }
}



Above this is the code, and this is fine if the API call works but if the call fails I will get an error response, so how to decode that struct in a combined way? I don't want to write another call for that and I am hoping to get something to do with Failure in the combine. or CAN I MAKE THE associated type (see protocol) generic?

CodePudding user response:

I beg for your patience. I think I understand the problem, but I'm having a hard time lining it up to the code you've given. Your fetchFunction is particularly odd and I don't understand what your protocol is trying to accomplish.

Let me start with the problem statement and explore a solution. I'll do it step-by-step so this will be a long response. The tl;dr is a Playground at the end.

I have an API and if my data is valid the API will give the correct response, for which I need to decode with the respective struct in swift.
If my data is wrong the API will fail and it will produce an error response which is a different struct.

So we need two structs. One for success and one for failure. I'll make some up:

struct SuccessfulResult : Decodable {
    let interestingText : String
}

struct FailedResult : Decodable {
    let errorCode : Int
    let failureReason : String
}

Based on that, request to the network can:

  • Return success data to decode into SuccessfulResult
  • Return failure data to decode into FailedResult
  • Fail because of a low-level error (e.g. The network is unreachable).

Let's create a type for "The network worked just fine and gave me either success data or failure data":

enum NetworkData {
    case success(Data)
    case failure(Data)
}

I'll use Error for low-level errors.

With those types an API request can be represented as a publisher of the type AnyPublisher<NetworkData, Error>

But that's not what you asked for. You want to parse the data into SuccessfulResult or FailedResult. This also raises the possibility that JSON parsing fails which I will also sweep under the rug of a generic Error.

We need a data type to represent the parsed variant of NetworkData:

enum ParsedNetworkData {
    case success(SuccessfulResult)
    case failure(FailedResult)
}

Which means the real Network request type you've asked for is a publisher of the type AnyPublisher<ParsedNetworkData,Error>

We can write a function to transform a Data bearing network request, AnyPublisher<NetworkData,Error>, into an AnyPublisher<ParsedNetworkData,Error>.

One way to write that function is:

func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {

    let decoder = JSONDecoder()
    return networkRequest
        .tryMap { networkData -> ParsedNetworkData in
            switch(networkData) {
                case .success(let successData):
                    return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
                case .failure(let failureData):
                    return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
            }
        }
        .eraseToAnyPublisher()
}

To exercise the code we can write a function to create a fake network request and add some code that tries things out. Putting it all together into a playground you get:

import Foundation
import Combine

struct SuccessfulResult : Decodable {
    let interestingText : String
}

struct FailedResult : Decodable {
    let errorCode : Int
    let failureReason : String
}

enum NetworkData {
    case success(Data)
    case failure(Data)
}

enum ParsedNetworkData {
    case success(SuccessfulResult)
    case failure(FailedResult)
}

func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {

    let decoder = JSONDecoder()
    return networkRequest
        .tryMap { networkData -> ParsedNetworkData in
            switch(networkData) {
                case .success(let successData):
                    return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
                case .failure(let failureData):
                    return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
            }
        }
        .eraseToAnyPublisher()
}

func fakeNetworkRequest(shouldSucceed: Bool) -> AnyPublisher<NetworkData,Error> {
    let successfulBody = """
    { "interestingText" : "This is interesting!" }
    """.data(using: .utf8)!

    let failedBody = """
    {
      "errorCode" : -4242,
      "failureReason" : "Bogus! Stuff went wrong."
    }
    """.data(using: .utf8)!

    return Future<NetworkData,Error> { fulfill in
        let delay = Set(stride(from: 100, to: 600, by: 100)).randomElement()!

        DispatchQueue.global(qos: .background).asyncAfter(
            deadline: .now()   .milliseconds(delay)) {
                if(shouldSucceed) {
                    fulfill(.success(NetworkData.success(successfulBody)))
                } else {
                    fulfill(.success(NetworkData.failure(failedBody)))
                }
        }
    }.eraseToAnyPublisher()
}

var subscriptions = Set<AnyCancellable>()
let successfulRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: true))
successfulRequest
    .sink(receiveCompletion:{ debugPrint($0) },
          receiveValue:{ debugPrint("Success Result \($0)") })
    .store(in: &subscriptions)

let failedRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: false))
failedRequest
    .sink(receiveCompletion:{ debugPrint($0) },
          receiveValue:{ debugPrint("Failure Result \($0)") })
    .store(in: &subscriptions)
  • Related