apollographql / apollo-kotlin

:rocket:  A strongly-typed, caching GraphQL client for the JVM, Android, and Kotlin multiplatform.
https://www.apollographql.com/docs/kotlin
MIT License
3.76k stars 653 forks source link

Have a way to specify a global custom `FakeResolver` for data builders #4435

Open eduardb opened 2 years ago

eduardb commented 2 years ago

Use case

Using data builders when having custom scalars requires a custom FakeResolver that implements resolveLeaf for those custom scalars if you want those scalars to be generated instead of having to manually specify them for each operation. The problem is that you need to pass this every single time you want to use a data builder for an operation, which can become a bit tiresome, and error-prone (e.g. if you forget to actually pass it down, and your tests start failing 😄).

Describe the solution you'd like

No strong opinions on how this should look like, I imagine that it could be a Gradle config like customFakeResolver.set("fully.qualified.class.name"), or some other API to specify a class that implements FakeResolver that would be used instead of DefaultFakeResolver by each generated data builder.

PS: I would keep the DefaultFakeResolver class though, as it's very useful to delegate to it for non-custom scalar leafs.

adapap commented 1 year ago

In my use case, even just being able to parse the custom scalars as specified in customScalarsMapping in build.gradle by default would be great!

martinbonnin commented 1 year ago

@adapap can you elaborate a bit more? This should work already:

// build.gradle.kts
apollo {
  service("service") {
    packageName.set("com.example")
    generateDataBuilders.set(true)
    mapScalar("Long", "com.example.MyLong")
  }
}
val data = GetCustomScalarQuery.Data {
  long = MyLong(42)
}

Or do you need something else?

adapap commented 1 year ago

@martinbonnin In my case, the default fake resolver does not seem to be respecting the scalars defined in build.gradle. For example, if I define my apollo config as such:

apollo {
  service("example") {
    packageName.set("com.example.app")
    generateDataBuilders.set(true)
    mapScalar("int64", "kotlin.Long")
    // We actually define custom scalars as such:
    // customScalarsMapping = rootProject.ext.universalApolloScalarAdapters

    schemaFile.set(file("../schema-copied.json"))
    srcDir(file("src/main/graphql/"))
  }
}

When I try to use DefaultFakeResolver with a query that uses an int64 type, I get the following error:

Don't know how to instantiate leaf int64
java.lang.IllegalStateException: Don't know how to instantiate leaf int64
    at com.apollographql.apollo3.api.DefaultFakeResolver.resolveLeaf(fakeResolver.kt:269)
    at com.apollographql.apollo3.api.FakeResolverKt.buildFieldOfNonNullType(fakeResolver.kt:212)
    at com.apollographql.apollo3.api.FakeResolverKt.buildFieldOfType(fakeResolver.kt:156)
    at com.apollographql.apollo3.api.FakeResolverKt.buildFieldOfNonNullType(fakeResolver.kt:209)
    at com.apollographql.apollo3.api.FakeResolverKt.buildFieldOfType(fakeResolver.kt:156)
    at com.apollographql.apollo3.api.FakeResolverKt.buildFieldOfType(fakeResolver.kt:151)
    at com.apollographql.apollo3.api.FakeResolverKt.buildFieldOfNonNullType(fakeResolver.kt:209)
    at com.apollographql.apollo3.api.FakeResolverKt.buildFieldOfType(fakeResolver.kt:156)
    at com.apollographql.apollo3.api.FakeResolverKt.buildFieldOfType(fakeResolver.kt:151)
    at com.apollographql.apollo3.api.FakeResolverKt.buildFieldOfNonNullType(fakeResolver.kt:197)
    at com.apollographql.apollo3.api.FakeResolverKt.buildFieldOfType(fakeResolver.kt:156)
    at com.apollographql.apollo3.api.FakeResolverKt.buildFakeObject(fakeResolver.kt:112)
    at com.apollographql.apollo3.api.FakeResolverKt.buildData(fakeResolver.kt:327)

Update: I also get an issue using data builders where there is a conflict trying to construct the data: Cannot access class 'com.example.app.MyQuery.InnerType'. Check your module classpath for missing or conflicting dependencies

MyQuery.Data {
  user = buildUser {
    innerType = buildInnerType {
      name = "test"
    }
  }
}
martinbonnin commented 1 year ago

@adapap thanks for sending this! As a side note for next time, can you open different issues? It helps keeping the discussion focused. But now that you're here, let's dive in!

When I try to use DefaultFakeResolver with a query that uses an int64 type, I get the following error

The DefaultFakeResolver doesn't know about the int64 custom scalar adapter as it's registered at runtime.

You can register the int64 custom scalar adapter it at build time to have this working:

apollo {
  service("example") {
    // This will use the builtin LongAdapter (you can remove the call to `addCustomScalarAdapter()`)
    mapScalarToKotlinLong("int64")
    // ...
  }
}

I also get an issue using data builders where there is a conflict trying to construct the data

That's unexpected. Can you share your schema ?

adapap commented 1 year ago

@martinbonnin I believe the issue was related to the issue described here: https://github.com/apollographql/apollo-kotlin/issues/4669

I'm curious in relation to the above - the issue is marked as closed in both this repository and in the issue filed in Google's issue tracker (https://issuetracker.google.com/issues/268218176). Is the expected resolution going forward to always include this snippet for Android modules?

outputDirConnection {
    connectToAndroidSourceSet("main")
}
adapap commented 1 year ago

You can register the int64 custom scalar adapter it at build time to have this working:

apollo {
service("example") {
// This will use the builtin LongAdapter (you can remove the call to `addCustomScalarAdapter()`)
mapScalarToKotlinLong("int64")
// ...
}
}

@martinbonnin This solution did not solve the issue for me, and in v3.8.1 I am not able to use data builders for tests without passing in a custom resolver. I get the Don't know how to instantiate leaf int64 error even if I manually specify all of the fields in the builder.

ar-g commented 11 months ago

This is very much needed! cc @martinbonnin

jamesonwilliams commented 3 months ago

I'm encountering this one as well -

In my schema module, I have:

apollo {
  service("ServiceName") {
    ...
    mapScalarToKotlinLong("ScalarName")
  }
}

I experimented with adding

ApolloClient.Builder()
  .addCustomScalarAdapter(ScalarName.type, LongAdapter)

But that seemed to make things worse.

I can get the tests to pass if I provide this resolver to my data builder's constructor:

class Resolver : DefaultFakeResolver(__Schema.all) {
  override fun resolveLeaf(context: FakeResolverContext): Any {
    return when (context.mergedField.type.rawType().name) {
      "ScalarName" -> 100L
      else -> super.resolveLeaf(context)
    }
  }
}

But, it would be sweet if mapScalarToKotlinLong could do it alone!

For what it's worth, the generated __Schema.all list does not include my ScalarName.type. (Not sure if it should!)

martinbonnin commented 3 months ago

@jamesonwilliams indeed scalars are a bit awkward because they are a runtime thing only. The codegen doesn't know how to instanciate a given custom scalar. For Long, 100L is a valid initializer but for a date, maybe we want Instant.now() or DateTime.parse(isoString), the sky is the limit in how complex that expression can be...