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