How can I return a generic type that implements another protocol

Viewed 146

I have networking module that implements a standard interface for exposing my networking client

protocol HTTPClientTask {
    func cancel()
}
protocol HTTPClient {
    @discardableResult
    func execute(_ request: URLRequest, _ completion: @escaping (Result<(response: HTTPURLResponse, data: Data), Error>) -> Void) -> HTTPClientTask
}

This is implemented something like

final class URLSessionHTTPClient: HTTPClient {
    
    private let session: URLSession
    private struct RequestError: Error { }
    private struct URLSessionTaskWrapper: HTTPClientTask {
        let wrapped: URLSessionTask
        func cancel() {
            wrapped.cancel()
        }
    }
    
    init(session: URLSession = .shared) {
        self.session = session
    }
    
    func execute(_ request: URLRequest, _ completion: @escaping (Result<(response: HTTPURLResponse, data: Data), Error>) -> Void) -> HTTPClientTask {
        let task = session.dataTask(with: request) { data, response, error in
            completion(Result {
                if let error = error {
                    throw error
                } else if let data = data, let response = response as? HTTPURLResponse {
                    return (response, data)
                } else {
                    throw RequestError()
                }
            })
        }
        task.resume()
        return URLSessionTaskWrapper(wrapped: task)
    }
}

An example to run in a playground would be

let requestURL = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!

let httpClient = URLSessionHTTPClient()

httpClient.execute(.init(url: requestURL)) { result in
    if let code = try? result.get().response.statusCode {
        print(code)
    }
}

I have another framework I'd like to add to my app that exposes an interface that is identical.

I don't want my other module to have a dependancy on this networking module, instead I'd prefer it to expose the interface it requires and have my networking module be dependant on it.

So the following interface is exposed by my other module

protocol AuthHTTPClientTask {
    func cancel()
}

protocol AuthHTTPClient {
    @discardableResult
    func execute(_ request: URLRequest, _ completion: @escaping (Result<(response: HTTPURLResponse, data: Data), Error>) -> Void) -> AuthHTTPClientTask
}

As you can see the interfaces are exact, however I was hoping to avoid creating an entire URLSessionHTTPClient just for that interface - URLSessionAuthHTTPClient or something as the behaviour etc is the same.

Is it possible to create some sort of type that URLSessionHTTPClient implements that allows it to return AuthHTTPClientTask OR HTTPClientTask?

I thought I could do something like

extension HTTPClientTask: AuthHTTPClientTask { }

final class URLSessionHTTPClient: HTTPClient, AuthHTTPClient {
    
    private let session: URLSession
    private struct RequestError: Error { 
.........

But this produces Extension of protocol 'HTTPClientTask' cannot have an inheritance clause and Type 'URLSessionHTTPClient' does not conform to protocol 'AuthHTTPClient'

1 Answers

You are getting the error about inheritance clause as a constraint is required for conformance purposes, in your example the compiler thinks it is an inheritance.

You could create a generic, private method that handles dispatching the request and extend the return types of that method to conform to your module types.

protocol HTTPClientTask {
    func cancel()
}

protocol HTTPClient {
    @discardableResult
    func execute(_ request: URLRequest, _ completion: @escaping (Result<(response: HTTPURLResponse, data: Data), Error>) -> Void) -> HTTPClientTask
}

protocol AuthHTTPClientTask {
    func cancel()
}

protocol AuthHTTPClient {
    @discardableResult
    func execute(_ request: URLRequest, _ completion: @escaping (Result<(response: HTTPURLResponse, data: Data), Error>) -> Void) -> AuthHTTPClientTask
}


private struct URLSessionTaskWrapper {
    let wrapped: URLSessionTask
    func cancel() {
        wrapped.cancel()
    }
}

extension URLSessionTaskWrapper: HTTPClientTask { }
extension URLSessionTaskWrapper: AuthHTTPClientTask { }

final class URLSessionHTTPClient {
    
    private let session: URLSession
    private struct RequestError: Error { }

    init(session: URLSession = .shared) {
        self.session = session
    }
    
    private func dispatch(_ request: URLRequest, _ completion: @escaping (Result<(response: HTTPURLResponse, data: Data), Error>) -> Void) -> URLSessionTaskWrapper {
        let task = session.dataTask(with: request) { data, response, error in
            completion(Result {
                if let error = error {
                    throw error
                } else if let data = data, let response = response as? HTTPURLResponse {
                    return (response, data)
                } else {
                    throw RequestError()
                }
            })
        }
        task.resume()
        return URLSessionTaskWrapper(wrapped: task)
    }
}


extension URLSessionHTTPClient: HTTPClient {
    func execute(_ request: URLRequest, _ completion: @escaping (Result<(response: HTTPURLResponse, data: Data), Error>) -> Void) -> HTTPClientTask {
        return dispatch(request, completion)
    }
}

extension URLSessionHTTPClient: AuthHTTPClient {
    func execute(_ request: URLRequest, _ completion: @escaping (Result<(response: HTTPURLResponse, data: Data), Error>) -> Void) -> AuthHTTPClientTask {
        return dispatch(request, completion)
    }
}

let requestURL = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!

let httpClient: HTTPClient = URLSessionHTTPClient()
httpClient.execute(.init(url: requestURL)) { result in
    if let code = try? result.get().response.statusCode {
        print("HTTP", code)
    }
}

let authzHTTPClient: AuthHTTPClient = URLSessionHTTPClient()
authzHTTPClient.execute(.init(url: requestURL)) { result in
    if let code = try? result.get().response.statusCode {
        print("Authz", code)
    }
}

Related