Search code examples
iosswiftswift-protocolsurlsession

How can I return a generic type that implements another protocol


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'


Solution

  • 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)
        }
    }