1

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.

5
  • What are you trying to achieve? Why do you need to specify type of client Commented Mar 11, 2019 at 14:39
  • 1
    There are multiple endpoints, the original code was written to make use of generics, for example Client<RolesAPI>() and so on. Commented Mar 11, 2019 at 14:40
  • But what is your exact need? Exact usage Commented Mar 11, 2019 at 15:02
  • 1
    I would like to be able to setup my api calls using a simple syntax such as client.request(.fetchProfileForUser(id: "123")) with the enum value within request(...) being set using a case from an enum that conforms to EndpointType. That enum is set when the instance of client is declared. Commented Mar 11, 2019 at 15:11
  • 1
    It is very similar to how Moya works, however I did not write this app, I have only inherited the codebase so I am not 100% clear on the reasoning originally. Commented Mar 11, 2019 at 15:12

1 Answer 1

2

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())
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.