Creating a @Dependency from ApolloClient

So far using TCA I've been able to create dependencies in our app fairly easily.

I created an ApiClient which can fetch from our back end. It doesn't have any generic constraint (it just returns (Data, URLResponse). This means I have one ApiClient for the whole app which is good. I then extend this ApiClient to add a generic function which fetches the (Data, URLResponse) and then decodes the data into a generic Decodable type.

I also created a DataRepo which does have a generic constraint as it creates a subscribable cache from an endpoint and has a generic constraint on its Resource type to know what to decode when it gets new values. This means I can have a DataRepo for each Resource which is fine as there are a limited number of them.

However... with this current problem I'm a bit stuck. I'm exploring using the ApolloClient for the first time and trying to work out how I can create a @Dependency for it to use within TCA.

Ideally at the call site it would look something like this...

// inside the reducer
case .someAction:
  return .run { send in
    do {
      let result = try await graphQLClient.fetch(SomeGraphQLQuery())
      send(.someSuccessAction(result))
    } catch {
      send(.someErrorAction)
    }
  }

However, I'm having difficulty getting this to work.

The problem is that the ApolloClient Documentation conforms to a protocol that uses generic functions.

In the case above the underlying ApolloClient function looks like this...

func fetch<Query>(
    query: Query,
    cachePolicy: CachePolicy,
    contextIdentifier: UUID?,
    queue: DispatchQueue,
    resultHandler: GraphQLResultHandler<Query.Data>?
) -> Cancellable where Query : GraphQLQuery

I'd like to do two things to this...

  1. Make it async await (which is the easy part).
  2. Wrap this in a struct like @Dependency so I can inject it and mock it etc... like every other dependency we have. (This is where I'm struggling).

The problem is that the generic constraint on the function defines both the input to the function Query and the output of the function Query.Data. I'm just not sure how to turn this into a Dependency without making the Type of the dependency generically constrained.

For instance, I could do this...

struct GraphQLClient<Query: GraphQLQuery> {
  let fetch: @Sendable (Query) async throws -> Query.Data
}

But now that means every query in the app will have its own separate GraphQLClient and they would all have to be defined as separate dependencies.

I have tried to use some of the new existential types to try and get around this. But being honest, I don't fully know how those work or what they're for yet.

I wonder if I could just create the GraphQLClient using an internal non-generic function (somehow) and then extend it (a bit like the ApiClient above) to add a public generic function to it?

Sorry for the rambling post. I hope I managed to get my problem across. Any advice or help would be greatly appreciated.

Thanks

1 Like

(NB: accidentally early posted so I deleted my first message)

Hi @Fogmeister, this question gets at a pretty complicated aspect of designing dependencies that we haven't yet really discussed publicly.

It boils down to there being a spectrum on which a dependency can lie: it is either super generic, but then difficult to mock for tests, or it is super specific, and then very easy to mock for tests.

Suppose for a moment that it was possible to use generics for properties. Then you could design your Apollo client like so:

struct ApolloClient {
  var fetch<Query: GraphQLQuery>: (Query) async throws -> Query.Data
}

You would construct the "live" version of this client by calling out to the real Apollo code under the hood, but how can you make a "mock" version for tests or previews? You would need to do something like this:

extension ApolloClient {
  static let mock = Self(
    fetch: { query in 
      // ???
    }
  )

You would need to somehow understand what query represents so that you could return some sensible mock data. But query is a very complex object since it is a full GraphQL query. In order to pick apart of a query object you would have to literally implement your own ad-hoc GraphQL query interpreter.

So, the client is super generic and makes for great call sites in your reducer since you can just construct a GraphQL query and hand it off to the fetch endpoint, but it essentially destroys the ability to mock it for tests and previews.

On the other hand, if you had designed a super specific client, something with an endpoint for each kind of query you want to make:

struct ApolloClient {
  var fetchUsers: () async throws -> [User]
  var fetchFriends: (User.ID) async throws -> [User]
  var updateUser: (User) async throws -> Void
}

Then the "live" implementation of this client could still make use of Apollo under the hood by constructing a GraphQL query, but then in tests and previews you can very easily mock:

extension ApolloClient {
  static let mock = Self(
    var fetchUsers: { [.mock] },
    var fetchFriends: { _ in [.mock] },
    var updateUser: { _ in }
  )
}

So, this client is extremely specific in that you have to add an additional endpoint for each kind of interaction you want to have with the API, but then it becomes extremely easy to mock for tests and previews.

This principle holds for other kinds of dependencies too. For example, say you needed a client for interacting with a database. You could model that as a super generic client that can take a SQL query string:

struct DatabaseClient {
  var query: (SQLQuery) -> any Codable
}

But then to mock this you would have to literally create your own SQLQuery interpreter to know what is being requested and how to construct some data to send back.

Or you could model it as a client with an endpoint for each kind of interaction you want to have with the database:

extension DatabaseClient {
  static let mock = Self(
    var fetchUsers: { [.mock] },
    var fetchFriends: { _ in [.mock] },
    var updateUser: { _ in }
  )
}

This is the exact approach we take in both isowords and Point-Free's server codebase specifically because it is so amenable to testing. It does mean a little more work to maintain the endpoints, but we think the benefits are huge. (This style also works very nicely with "unimplemented" dependencies where you get to prove exactly which parts of a dependency are used in a specific execution flow.)

One last thing I want to mention is the style of API client you mentioned at the beginning of your post where you have a single generic endpoint like this:

struct APIClient {
  var request: (URLRequest) async throws -> (Data, URLResponse)
}

This style of dependency has the same problems I have discussed above, but it's a little less pernicious. The "interpretation" of a URLRequest is a lot simpler, and so you may not notice it being a problem. You can just pluck off a few pieces of the request to roughly figure out what is being requested and then return the data:

extension APIClient {
  static let mock = Self(
    request: { request in 
      if request.pathComponents.last == "users" {
        return try JSONEncoder().encoder([User.mock])
      }
      …
    }
  )
}

You still are interpreting a kind of "query", but it's just a URLRequest and so a bit simpler.

So, you have to decide where do you want the complexity: when mocking for tests and previews, or when maintaining the endpoints for the client?


If you made it this far and you still want a super generic client, then you can, you just need to abandon the struct style of designing your dependency. And considering the complications with testing such dependencies, I'm not sure the struct style is buying you much.

So, instead of holding a concrete struct client in the @Dependency you will just hold an any ApolloClient, and then you can create conformances as you need.

8 Likes

Hi Brandon,

Thanks very much for the detailed answer.

I think I was trying to get closer to your answer of using multiple endpoints but I was trying to wrap up the queries into an enum that would then dispense a query. But it didn't get around the underlying issue of the generic constraint.

Your example of using a separate function for each request makes a lot of sense and gets rid of the generic constraint issue I had which would create a whole new dependency per query.

Thanks

I'll give that a go.

I'll also take a closer look at the isowords dependency you mentioned.

RE the ApiClient dependency we're using URLParsing and a Route enum (inspired by isowords) to create the dependency and override the endpoints by matching the Route rather than trying to match the URLRequest itself.

Yeah, our URL routing library was designed specifically to allow for a generic API client that comes with a "query" object that is easier to interpret and mock. Because the "request" is a well-structured enum of all the endpoints you get a very simple interface for mocking and overriding specific endpoints.

But no such interface exists for GraphQL or SQL clients (afaik).

4 Likes