Reputation: 1271
I am working on a project that has a network client that basically follows the below pattern.
protocol EndpointType {
var baseURL: String { get }
}
enum ProfilesAPI {
case fetchProfileForUser(id: String)
}
extension ProfilesAPI: EndpointType {
var baseURL: String {
return "https://foo.bar"
}
}
protocol ClientType: class {
associatedtype T: EndpointType
func request(_ request: T) -> Void
}
class Client<T: EndpointType>: ClientType {
func request(_ request: T) -> Void {
print(request.baseURL)
}
}
let client = Client<ProfilesAPI>()
client.request(.fetchProfileForUser(id: "123"))
As part of tidying up this project and writing tests I have found the it is not possible to inject a client
when conforming to the ClientType
protocol.
let client: ClientType = Client<ProfilesAPI>()
produces an error:
error: member 'request' cannot be used on value of protocol type 'ClientType'; use a generic constraint instead
I would like to maintain the current pattern ... = Client<ProfilesAPI>()
Is it possible to achieve this using type erasure? I have been reading but am not sure how to make this work.
Upvotes: 1
Views: 257
Reputation: 299325
To your actual question, the type eraser is straight-forward:
final class AnyClient<T: EndpointType>: ClientType {
let _request: (T) -> Void
func request(_ request: T) { _request(request) }
init<Client: ClientType>(_ client: Client) where Client.T == T {
_request = client.request
}
}
You'll need one of these _func/func
pairs for each requirement in the protocol. You can use it this way:
let client = AnyClient(Client<ProfilesAPI>())
And then you can create a testing harness like:
class RecordingClient<T: EndpointType>: ClientType {
var requests: [T] = []
func request(_ request: T) -> Void {
requests.append(request)
print("recording: \(request.baseURL)")
}
}
And use that one instead:
let client = AnyClient(RecordingClient<ProfilesAPI>())
But I don't really recommend this approach if you can avoid it. Type erasers are a headache. Instead, I would look inside of Client
, and extract the non-generic part into a ClientEngine
protocol that doesn't require T
. Then make that swappable when you construct the Client
. Then you don't need type erasers, and you don't have to expose an extra protocol to the callers (just EndpointType).
For example, the engine part:
protocol ClientEngine: class {
func request(_ request: String) -> Void
}
class StandardClientEngine: ClientEngine {
func request(_ request: String) -> Void {
print(request)
}
}
The client that holds an engine. Notice how it uses a default parameter so that callers don't have to change anything.
class Client<T: EndpointType> {
let engine: ClientEngine
init(engine: ClientEngine = StandardClientEngine()) { self.engine = engine }
func request(_ request: T) -> Void {
engine.request(request.baseURL)
}
}
let client = Client<ProfilesAPI>()
And again, a recording version:
class RecordingClientEngine: ClientEngine {
var requests: [String] = []
func request(_ request: String) -> Void {
requests.append(request)
print("recording: \(request)")
}
}
let client = Client<ProfilesAPI>(engine: RecordingClientEngine())
Upvotes: 2