Skip to content

Commit 5812fa0

Browse files
smyrickdariuszkuc
authored andcommitted
fix: *generateGraphQLType hooks are now called on interfaces (#303)
* fix: *generateGraphQLType hooks are now called on interfaces Fixes #294 Interfaces now go through the internal TypeBuilder method 'graphQLTypeOf' instead of 'objectFromReflection'. This means we can make 'objectFromReflection' private. But the type may be (if not always) wrapped in a GraphQLNonNull which means we have to cast it back to the interface type before adding it to the ObjectBuilder.
1 parent 4c2652f commit 5812fa0

File tree

14 files changed

+144
-59
lines changed

14 files changed

+144
-59
lines changed

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/SchemaGenerator.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import com.expedia.graphql.generator.types.SubscriptionBuilder
1818
import com.expedia.graphql.generator.types.UnionBuilder
1919
import graphql.schema.GraphQLCodeRegistry
2020
import graphql.schema.GraphQLDirective
21-
import graphql.schema.GraphQLInterfaceType
2221
import graphql.schema.GraphQLSchema
2322
import java.lang.reflect.Field
2423
import kotlin.reflect.KAnnotatedElement
@@ -76,8 +75,8 @@ open class SchemaGenerator(val config: SchemaGeneratorConfig) {
7675
open fun listType(type: KType, inputType: Boolean) =
7776
listTypeBuilder.listType(type, inputType)
7877

79-
open fun objectType(kClass: KClass<*>, interfaceType: GraphQLInterfaceType? = null) =
80-
objectTypeBuilder.objectType(kClass, interfaceType)
78+
open fun objectType(kClass: KClass<*>) =
79+
objectTypeBuilder.objectType(kClass)
8180

8281
open fun inputObjectType(kClass: KClass<*>) =
8382
inputObjectTypeBuilder.inputObjectType(kClass)

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/SubTypeMapper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ internal class SubTypeMapper(supportedPackages: List<String>) {
77

88
private val reflections = Reflections(supportedPackages)
99

10-
fun getSubTypesOf(kclass: KClass<*>): List<Class<*>> = reflections.getSubTypesOf(kclass.java).filterNot { it.kotlin.isAbstract }
10+
fun getSubTypesOf(kclass: KClass<*>): List<KClass<*>> = reflections.getSubTypesOf(kclass.java).filterNot { it.kotlin.isAbstract }.map { it.kotlin }
1111
}

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/TypeBuilder.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.expedia.graphql.generator.state.TypesCacheKey
1313
import graphql.schema.GraphQLCodeRegistry
1414
import graphql.schema.GraphQLType
1515
import graphql.schema.GraphQLTypeReference
16+
import graphql.schema.GraphQLTypeUtil
1617
import kotlin.reflect.KClass
1718
import kotlin.reflect.KType
1819

@@ -28,11 +29,17 @@ internal open class TypeBuilder constructor(protected val generator: SchemaGener
2829
?: generator.scalarType(type, annotatedAsID)
2930
?: objectFromReflection(type, inputType)
3031

31-
val typeWithNullability = graphQLType.wrapInNonNull(type)
32-
return config.hooks.didGenerateGraphQLType(type, typeWithNullability)
32+
// Do not call the hook on GraphQLTypeReference as we have not generated the type yet
33+
val unwrappedType = GraphQLTypeUtil.unwrapNonNull(graphQLType)
34+
if (unwrappedType !is GraphQLTypeReference) {
35+
val typeWithNullability = graphQLType.wrapInNonNull(type)
36+
return config.hooks.didGenerateGraphQLType(type, typeWithNullability)
37+
}
38+
39+
return graphQLType
3340
}
3441

35-
internal fun objectFromReflection(type: KType, inputType: Boolean): GraphQLType {
42+
private fun objectFromReflection(type: KType, inputType: Boolean): GraphQLType {
3643
val cacheKey = TypesCacheKey(type, inputType)
3744
val cachedType = state.cache.get(cacheKey)
3845

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.expedia.graphql.generator.extensions
2+
3+
import kotlin.reflect.KFunction
4+
import kotlin.reflect.KParameter
5+
import kotlin.reflect.full.valueParameters
6+
7+
internal fun KFunction<*>.getValidArguments(): List<KParameter> =
8+
this.valueParameters
9+
.filterNot { it.isGraphQLContext() }
10+
.filterNot { it.isGraphQLIgnored() }
11+
.filterNot { it.isDataFetchingEnvironment() }

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/state/TypesCache.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ internal class TypesCache(private val supportedPackages: List<String>) {
3939
val cacheKey = getCacheKeyString(key)
4040

4141
if (cacheKey != null) {
42-
typeUnderConstruction.remove(kGraphQLType.kClass)
43-
return cache.put(cacheKey, kGraphQLType)
42+
cache[cacheKey] = kGraphQLType
43+
return kGraphQLType
4444
}
4545

4646
return null

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/types/FunctionBuilder.kt

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ import com.expedia.graphql.generator.extensions.getGraphQLDescription
1010
import com.expedia.graphql.generator.extensions.getKClass
1111
import com.expedia.graphql.generator.extensions.getName
1212
import com.expedia.graphql.generator.extensions.getTypeOfFirstArgument
13-
import com.expedia.graphql.generator.extensions.isDataFetchingEnvironment
14-
import com.expedia.graphql.generator.extensions.isGraphQLContext
15-
import com.expedia.graphql.generator.extensions.isGraphQLIgnored
13+
import com.expedia.graphql.generator.extensions.getValidArguments
1614
import com.expedia.graphql.generator.extensions.isInterface
1715
import com.expedia.graphql.generator.extensions.safeCast
1816
import graphql.schema.FieldCoordinates
@@ -25,7 +23,6 @@ import kotlin.reflect.KFunction
2523
import kotlin.reflect.KParameter
2624
import kotlin.reflect.KType
2725
import kotlin.reflect.full.isSubclassOf
28-
import kotlin.reflect.full.valueParameters
2926

3027
internal class FunctionBuilder(generator: SchemaGenerator) : TypeBuilder(generator) {
3128

@@ -44,10 +41,7 @@ internal class FunctionBuilder(generator: SchemaGenerator) : TypeBuilder(generat
4441
builder.withDirective(it)
4542
}
4643

47-
fn.valueParameters
48-
.filterNot { it.isGraphQLContext() }
49-
.filterNot { it.isGraphQLIgnored() }
50-
.filterNot { it.isDataFetchingEnvironment() }
44+
fn.getValidArguments()
5145
.forEach {
5246
// deprecation of arguments is currently unsupported: https://github.com/facebook/graphql/issues/197
5347
builder.argument(argument(it))

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/types/InterfaceBuilder.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import graphql.TypeResolutionEnvironment
1111
import graphql.schema.GraphQLInterfaceType
1212
import graphql.schema.GraphQLType
1313
import graphql.schema.GraphQLTypeReference
14+
import graphql.schema.GraphQLTypeUtil
1415
import kotlin.reflect.KClass
16+
import kotlin.reflect.full.createType
1517

1618
internal class InterfaceBuilder(generator: SchemaGenerator) : TypeBuilder(generator) {
1719
internal fun interfaceType(kClass: KClass<*>): GraphQLType {
@@ -31,15 +33,17 @@ internal class InterfaceBuilder(generator: SchemaGenerator) : TypeBuilder(genera
3133
kClass.getValidFunctions(config.hooks)
3234
.forEach { builder.field(generator.function(it, kClass.getSimpleName(), abstract = true)) }
3335

34-
val interfaceType = builder.build()
35-
val implementations = subTypeMapper.getSubTypesOf(kClass)
36-
implementations.forEach { implementation ->
37-
val objectType = generator.objectType(implementation.kotlin, interfaceType)
38-
// skip objects currently under construction
39-
if (objectType !is GraphQLTypeReference) {
40-
state.additionalTypes.add(objectType)
36+
subTypeMapper.getSubTypesOf(kClass)
37+
.map { graphQLTypeOf(it.createType()) }
38+
.forEach {
39+
// Do not add objects currently under construction to the additional types
40+
val unwrappedType = GraphQLTypeUtil.unwrapNonNull(it)
41+
if (unwrappedType !is GraphQLTypeReference) {
42+
state.additionalTypes.add(it)
43+
}
4144
}
42-
}
45+
46+
val interfaceType = builder.build()
4347
codeRegistry.typeResolver(interfaceType) { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObject<Any>().javaClass.kotlin.getSimpleName()) }
4448
config.hooks.onRewireGraphQLType(interfaceType).safeCast()
4549
}

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/types/ObjectBuilder.kt

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import graphql.schema.GraphQLInterfaceType
1212
import graphql.schema.GraphQLObjectType
1313
import graphql.schema.GraphQLType
1414
import graphql.schema.GraphQLTypeReference
15+
import graphql.schema.GraphQLTypeUtil
1516
import kotlin.reflect.KClass
1617
import kotlin.reflect.full.createType
1718

1819
internal class ObjectBuilder(generator: SchemaGenerator) : TypeBuilder(generator) {
1920

20-
internal fun objectType(kClass: KClass<*>, interfaceType: GraphQLInterfaceType? = null): GraphQLType {
21+
internal fun objectType(kClass: KClass<*>): GraphQLType {
2122
return state.cache.buildIfNotUnderConstruction(kClass) {
2223
val builder = GraphQLObjectType.newObject()
2324

@@ -29,18 +30,14 @@ internal class ObjectBuilder(generator: SchemaGenerator) : TypeBuilder(generator
2930
builder.withDirective(it)
3031
}
3132

32-
if (interfaceType != null) {
33-
// invoked from the interface builder which can still be modified by the hooks
34-
builder.withInterface(GraphQLTypeReference(interfaceType.name))
35-
} else {
36-
kClass.getValidSuperclasses(config.hooks)
37-
.map { objectFromReflection(it.createType(), false) }
38-
.forEach {
39-
when (it) {
40-
is GraphQLInterfaceType -> builder.withInterface(it)
41-
}
33+
kClass.getValidSuperclasses(config.hooks)
34+
.map { graphQLTypeOf(it.createType()) }
35+
.forEach {
36+
when (val unwrappedType = GraphQLTypeUtil.unwrapNonNull(it)) {
37+
is GraphQLTypeReference -> builder.withInterface(unwrappedType)
38+
is GraphQLInterfaceType -> builder.withInterface(unwrappedType)
4239
}
43-
}
40+
}
4441

4542
kClass.getValidProperties(config.hooks)
4643
.forEach { builder.field(generator.property(it, kClass)) }

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/types/UnionBuilder.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import com.expedia.graphql.generator.SchemaGenerator
44
import com.expedia.graphql.generator.TypeBuilder
55
import com.expedia.graphql.generator.extensions.getGraphQLDescription
66
import com.expedia.graphql.generator.extensions.getSimpleName
7-
import com.expedia.graphql.generator.state.TypesCacheKey
87
import graphql.TypeResolutionEnvironment
98
import graphql.schema.GraphQLObjectType
109
import graphql.schema.GraphQLType
1110
import graphql.schema.GraphQLTypeReference
11+
import graphql.schema.GraphQLTypeUtil
1212
import graphql.schema.GraphQLUnionType
1313
import kotlin.reflect.KClass
1414
import kotlin.reflect.full.createType
@@ -24,16 +24,15 @@ internal class UnionBuilder(generator: SchemaGenerator) : TypeBuilder(generator)
2424
builder.withDirective(it)
2525
}
2626

27-
val implementations = subTypeMapper.getSubTypesOf(kClass)
28-
implementations.forEach {
29-
val objectType = state.cache.get(TypesCacheKey(it.kotlin.createType(), false))
30-
?: generator.objectType(it.kotlin)
31-
32-
when (objectType) {
33-
is GraphQLTypeReference -> builder.possibleType(objectType)
34-
is GraphQLObjectType -> builder.possibleType(objectType)
27+
subTypeMapper.getSubTypesOf(kClass)
28+
.map { graphQLTypeOf(it.createType()) }
29+
.forEach {
30+
when (val unwrappedType = GraphQLTypeUtil.unwrapNonNull(it)) {
31+
is GraphQLTypeReference -> builder.possibleType(unwrappedType)
32+
is GraphQLObjectType -> builder.possibleType(unwrappedType)
33+
}
3534
}
36-
}
35+
3736
val unionType = builder.build()
3837
codeRegistry.typeResolver(unionType) { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObject<Any>().javaClass.kotlin.getSimpleName()) }
3938
config.hooks.onRewireGraphQLType(unionType)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.expedia.graphql.generator.extensions
2+
3+
import com.expedia.graphql.annotations.GraphQLContext
4+
import com.expedia.graphql.annotations.GraphQLIgnore
5+
import graphql.schema.DataFetchingEnvironment
6+
import org.junit.jupiter.api.Test
7+
import kotlin.test.assertEquals
8+
9+
internal class KFunctionExtensionsKtTest {
10+
11+
@Test
12+
fun getValidArguments() {
13+
val args = TestingClass::happyPath.getValidArguments()
14+
assertEquals(expected = 1, actual = args.size)
15+
assertEquals(expected = "color", actual = args.first().getName())
16+
}
17+
18+
@Test
19+
fun `getValidArguments should ignore @GraphQLIgnore`() {
20+
val args = TestingClass::ignored.getValidArguments()
21+
assertEquals(expected = 1, actual = args.size)
22+
assertEquals(expected = "notIgnored", actual = args.first().getName())
23+
}
24+
25+
@Test
26+
fun `getValidArguments should ignore @GraphQLContext`() {
27+
val args = TestingClass::context.getValidArguments()
28+
assertEquals(expected = 1, actual = args.size)
29+
assertEquals(expected = "notContext", actual = args.first().getName())
30+
}
31+
32+
@Test
33+
fun `getValidArguments should ignore DataFetchingEnvironment`() {
34+
val args = TestingClass::dataFetchingEnvironment.getValidArguments()
35+
assertEquals(expected = 1, actual = args.size)
36+
assertEquals(expected = "notEnvironment", actual = args.first().getName())
37+
}
38+
39+
private class TestingClass {
40+
fun happyPath(color: String) = "You're color is $color"
41+
42+
fun ignored(@GraphQLIgnore ignoredArg: String, notIgnored: String) = "$ignoredArg and $notIgnored"
43+
44+
fun context(@GraphQLContext contextArg: String, notContext: String) = "$contextArg and $notContext"
45+
46+
fun dataFetchingEnvironment(environment: DataFetchingEnvironment, notEnvironment: String): String = "${environment.field.name} and $notEnvironment"
47+
}
48+
}

0 commit comments

Comments
 (0)