apollo-kotlin icon indicating copy to clipboard operation
apollo-kotlin copied to clipboard

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

Open eduardb opened this issue 3 years ago • 9 comments

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.

eduardb avatar Oct 04 '22 16:10 eduardb

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!

adapap avatar Feb 01 '23 05:02 adapap

@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?

martinbonnin avatar Feb 01 '23 08:02 martinbonnin

@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"
    }
  }
}

adapap avatar Feb 15 '23 00:02 adapap

@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 ?

martinbonnin avatar Feb 15 '23 12:02 martinbonnin

@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 avatar May 11 '23 15:05 adapap

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.

adapap avatar Jun 22 '23 19:06 adapap

This is very much needed! cc @martinbonnin

ar-g avatar Dec 08 '23 14:12 ar-g

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!)

jamesonwilliams avatar Jul 11 '24 11:07 jamesonwilliams

@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...

martinbonnin avatar Jul 11 '24 11:07 martinbonnin