Skip to content

Commit 5605e24

Browse files
authored
Allow additional types to be added with custom SchemaGenerator (#587)
* Refactor additional types This is a patch change to allow additional types to more easily be extended in later PRs. As a side-effect of this change we actually simplify where types are all generated and the main generateSchema function becomes just a little more straight forward * Add more unit tests * Simplify generateAdditionalTypes logic After we have fully generated queries,mutations, and subscriptions there are no more type references. We only have this issue when we were adding classes in the middle of generate so this actually reduces logic and branches in the library * Remove new features for now Do not make the function open for outside calling right now. Discussing further with team and we can add the new feature in a separate PR
1 parent 7de847b commit 5605e24

File tree

12 files changed

+116
-39
lines changed

12 files changed

+116
-39
lines changed

graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/validation/validateProvidesDirective.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616

1717
package com.expediagroup.graphql.federation.validation
1818

19+
import com.expediagroup.graphql.extensions.unwrapType
1920
import com.expediagroup.graphql.federation.directives.PROVIDES_DIRECTIVE_NAME
2021
import com.expediagroup.graphql.federation.extensions.isExtendedType
2122
import graphql.schema.GraphQLFieldDefinition
2223
import graphql.schema.GraphQLObjectType
2324
import graphql.schema.GraphQLTypeReference
24-
import graphql.schema.GraphQLTypeUtil
2525

2626
// [OK] @provides on base type references valid @external fields on @extend object
2727
// [ERROR] @provides on base type references local object fields
@@ -30,7 +30,7 @@ import graphql.schema.GraphQLTypeUtil
3030
// [OK] @provides references list of valid @extend objects
3131
// [ERROR] @provides references @external list field
3232
// [ERROR] @provides references @external interface field
33-
internal fun validateProvidesDirective(federatedType: String, field: GraphQLFieldDefinition): List<String> = when (val returnType = GraphQLTypeUtil.unwrapType(field.type).last()) {
33+
internal fun validateProvidesDirective(federatedType: String, field: GraphQLFieldDefinition): List<String> = when (val returnType = field.type.unwrapType()) {
3434
is GraphQLObjectType -> {
3535
if (!returnType.isExtendedType()) {
3636
listOf("@provides directive is specified on a $federatedType.${field.name} field references local object")

graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/types/EntityTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package com.expediagroup.graphql.federation.types
1818

19-
import graphql.schema.GraphQLTypeUtil
19+
import com.expediagroup.graphql.extensions.unwrapType
2020
import graphql.schema.GraphQLUnionType
2121
import org.junit.jupiter.api.Test
2222
import kotlin.test.assertEquals
@@ -41,7 +41,7 @@ internal class EntityTest {
4141
assertFalse(result.description.isNullOrEmpty())
4242
assertEquals(expected = 1, actual = result.arguments.size)
4343

44-
val graphQLUnionType = GraphQLTypeUtil.unwrapType(result.type).last() as? GraphQLUnionType
44+
val graphQLUnionType = result.type.unwrapType() as? GraphQLUnionType
4545

4646
assertNotNull(graphQLUnionType)
4747
assertEquals(expected = "_Entity", actual = graphQLUnionType.name)
@@ -52,7 +52,7 @@ internal class EntityTest {
5252
@Test
5353
fun `generateEntityFieldDefinition should return a valid type on a multiple values`() {
5454
val result = generateEntityFieldDefinition(setOf("MyType", "MySecondType"))
55-
val graphQLUnionType = GraphQLTypeUtil.unwrapType(result.type).last() as? GraphQLUnionType
55+
val graphQLUnionType = result.type.unwrapType() as? GraphQLUnionType
5656

5757
assertNotNull(graphQLUnionType)
5858
assertEquals(expected = 2, actual = graphQLUnionType.types.size)

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/extensions/deepName.kt renamed to graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/extensions/graphqlTypeExtensions.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,10 @@ import graphql.schema.GraphQLTypeUtil
2828
*/
2929
val GraphQLType.deepName: String
3030
get() = GraphQLTypeUtil.simplePrint(this)
31+
32+
/**
33+
* Unwrap the type of all layers and return the last element.
34+
* This includes GraphQLNonNull and GraphQLList.
35+
* If the type is not wrapped, it will just be returned.
36+
*/
37+
fun GraphQLType.unwrapType(): GraphQLType = GraphQLTypeUtil.unwrapType(this).last()

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

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import graphql.schema.GraphQLSchema
3232
import graphql.schema.GraphQLType
3333
import java.util.concurrent.ConcurrentHashMap
3434
import kotlin.reflect.KClass
35+
import kotlin.reflect.KType
3536
import kotlin.reflect.full.createType
3637

3738
/**
@@ -45,7 +46,7 @@ open class SchemaGenerator(internal val config: SchemaGeneratorConfig) {
4546
internal val classScanner = ClassScanner(config.supportedPackages)
4647
internal val cache = TypesCache(config.supportedPackages)
4748
internal val codeRegistry = GraphQLCodeRegistry.newCodeRegistry()
48-
internal val additionalTypes = mutableSetOf<GraphQLType>()
49+
internal val additionalTypes = mutableSetOf<KType>()
4950
internal val directives = ConcurrentHashMap<String, GraphQLDirective>()
5051

5152
init {
@@ -68,16 +69,12 @@ open class SchemaGenerator(internal val config: SchemaGeneratorConfig) {
6869
mutations: List<TopLevelObject> = emptyList(),
6970
subscriptions: List<TopLevelObject> = emptyList()
7071
): GraphQLSchema {
72+
7173
val builder = GraphQLSchema.newSchema()
7274
builder.query(generateQueries(this, queries))
7375
builder.mutation(generateMutations(this, mutations))
7476
builder.subscription(generateSubscriptions(this, subscriptions))
75-
76-
// add unreferenced interface implementations
77-
additionalTypes.forEach {
78-
builder.additionalType(it)
79-
}
80-
77+
builder.additionalTypes(generateAdditionalTypes(additionalTypes))
8178
builder.additionalDirectives(directives.values.toSet())
8279
builder.codeRegistry(codeRegistry.build())
8380

@@ -94,10 +91,10 @@ open class SchemaGenerator(internal val config: SchemaGeneratorConfig) {
9491
* This is helpful for things like federation or combining external schemas
9592
*/
9693
protected fun addAdditionalTypesWithAnnotation(annotation: KClass<*>) {
97-
classScanner.getClassesWithAnnotation(annotation)
98-
.map { generateGraphQLType(this, it.createType()) }
99-
.forEach {
100-
additionalTypes.add(it)
101-
}
94+
classScanner.getClassesWithAnnotation(annotation).forEach {
95+
additionalTypes.add(it.createType())
96+
}
10297
}
98+
99+
private fun generateAdditionalTypes(additionalTypes: Set<KType>): Set<GraphQLType> = additionalTypes.map { generateGraphQLType(this, it) }.toSet()
103100
}

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/generateGraphQLType.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.expediagroup.graphql.generator.types
1818

19+
import com.expediagroup.graphql.extensions.unwrapType
1920
import com.expediagroup.graphql.generator.SchemaGenerator
2021
import com.expediagroup.graphql.generator.extensions.getKClass
2122
import com.expediagroup.graphql.generator.extensions.isEnum
@@ -26,7 +27,6 @@ import com.expediagroup.graphql.generator.extensions.wrapInNonNull
2627
import com.expediagroup.graphql.generator.state.TypesCacheKey
2728
import graphql.schema.GraphQLType
2829
import graphql.schema.GraphQLTypeReference
29-
import graphql.schema.GraphQLTypeUtil
3030
import kotlin.reflect.KClass
3131
import kotlin.reflect.KType
3232

@@ -40,7 +40,7 @@ internal fun generateGraphQLType(generator: SchemaGenerator, type: KType, inputT
4040
?: objectFromReflection(generator, type, inputType)
4141

4242
// Do not call the hook on GraphQLTypeReference as we have not generated the type yet
43-
val unwrappedType = GraphQLTypeUtil.unwrapType(graphQLType).lastElement()
43+
val unwrappedType = graphQLType.unwrapType()
4444
val typeWithNullability = graphQLType.wrapInNonNull(type)
4545
if (unwrappedType !is GraphQLTypeReference) {
4646
return generator.config.hooks.didGenerateGraphQLType(type, typeWithNullability)
@@ -58,9 +58,11 @@ private fun objectFromReflection(generator: SchemaGenerator, type: KType, inputT
5858
}
5959

6060
val kClass = type.getKClass()
61-
val graphQLType = generator.cache.buildIfNotUnderConstruction(kClass, inputType) { getGraphQLType(generator, kClass, inputType, type) }
6261

63-
return generator.config.hooks.willAddGraphQLTypeToSchema(type, graphQLType)
62+
return generator.cache.buildIfNotUnderConstruction(kClass, inputType) {
63+
val graphQLType = getGraphQLType(generator, kClass, inputType, type)
64+
generator.config.hooks.willAddGraphQLTypeToSchema(type, graphQLType)
65+
}
6466
}
6567

6668
private fun getGraphQLType(generator: SchemaGenerator, kClass: KClass<*>, inputType: Boolean, type: KType): GraphQLType = when {

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/generateInterface.kt

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import com.expediagroup.graphql.generator.extensions.getValidSuperclasses
2626
import com.expediagroup.graphql.generator.extensions.safeCast
2727
import graphql.TypeResolutionEnvironment
2828
import graphql.schema.GraphQLInterfaceType
29-
import graphql.schema.GraphQLTypeReference
30-
import graphql.schema.GraphQLTypeUtil
3129
import kotlin.reflect.KClass
3230
import kotlin.reflect.full.createType
3331

@@ -53,14 +51,7 @@ internal fun generateInterface(generator: SchemaGenerator, kClass: KClass<*>): G
5351
.forEach { builder.field(generateFunction(generator, it, kClass.getSimpleName(), null, abstract = true)) }
5452

5553
generator.classScanner.getSubTypesOf(kClass)
56-
.map { generateGraphQLType(generator, it.createType()) }
57-
.forEach {
58-
// Do not add objects currently under construction to the additional types
59-
val unwrappedType = GraphQLTypeUtil.unwrapType(it).last()
60-
if (unwrappedType !is GraphQLTypeReference) {
61-
generator.additionalTypes.add(it)
62-
}
63-
}
54+
.forEach { generator.additionalTypes.add(it.createType()) }
6455

6556
val interfaceType = builder.build()
6657
generator.codeRegistry.typeResolver(interfaceType) { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObject<Any>().javaClass.kotlin.getSimpleName()) }

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/generateObject.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.expediagroup.graphql.generator.types
1818

19+
import com.expediagroup.graphql.extensions.unwrapType
1920
import com.expediagroup.graphql.generator.SchemaGenerator
2021
import com.expediagroup.graphql.generator.extensions.getGraphQLDescription
2122
import com.expediagroup.graphql.generator.extensions.getSimpleName
@@ -26,7 +27,6 @@ import com.expediagroup.graphql.generator.extensions.safeCast
2627
import graphql.schema.GraphQLInterfaceType
2728
import graphql.schema.GraphQLObjectType
2829
import graphql.schema.GraphQLTypeReference
29-
import graphql.schema.GraphQLTypeUtil
3030
import kotlin.reflect.KClass
3131
import kotlin.reflect.full.createType
3232

@@ -44,7 +44,7 @@ internal fun generateObject(generator: SchemaGenerator, kClass: KClass<*>): Grap
4444
kClass.getValidSuperclasses(generator.config.hooks)
4545
.map { generateGraphQLType(generator, it.createType()) }
4646
.forEach {
47-
when (val unwrappedType = GraphQLTypeUtil.unwrapType(it).last()) {
47+
when (val unwrappedType = it.unwrapType()) {
4848
is GraphQLTypeReference -> builder.withInterface(unwrappedType)
4949
is GraphQLInterfaceType -> builder.withInterface(unwrappedType)
5050
}

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/generateUnion.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616

1717
package com.expediagroup.graphql.generator.types
1818

19+
import com.expediagroup.graphql.extensions.unwrapType
1920
import com.expediagroup.graphql.generator.SchemaGenerator
2021
import com.expediagroup.graphql.generator.extensions.getGraphQLDescription
2122
import com.expediagroup.graphql.generator.extensions.getSimpleName
2223
import com.expediagroup.graphql.generator.extensions.safeCast
2324
import graphql.TypeResolutionEnvironment
2425
import graphql.schema.GraphQLObjectType
2526
import graphql.schema.GraphQLTypeReference
26-
import graphql.schema.GraphQLTypeUtil
2727
import graphql.schema.GraphQLUnionType
2828
import kotlin.reflect.KClass
2929
import kotlin.reflect.full.createType
@@ -40,7 +40,7 @@ internal fun generateUnion(generator: SchemaGenerator, kClass: KClass<*>): Graph
4040
generator.classScanner.getSubTypesOf(kClass)
4141
.map { generateGraphQLType(generator, it.createType()) }
4242
.forEach {
43-
when (val unwrappedType = GraphQLTypeUtil.unwrapType(it).last()) {
43+
when (val unwrappedType = it.unwrapType()) {
4444
is GraphQLTypeReference -> builder.possibleType(unwrappedType)
4545
is GraphQLObjectType -> builder.possibleType(unwrappedType)
4646
}

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcherTest.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ internal class FunctionDataFetcherTest {
5151

5252
fun throwException() { throw GraphQLException("Test Exception") }
5353

54-
suspend fun suspendThrow(value: String?): String = coroutineScope {
55-
value ?: throw GraphQLException("Suspended Exception")
54+
suspend fun suspendThrow(): String = coroutineScope<String> {
55+
throw GraphQLException("Suspended Exception")
5656
}
5757

5858
@GraphQLName("myCustomField")
@@ -155,7 +155,6 @@ internal class FunctionDataFetcherTest {
155155
fun `suspendThrow throws exception when resolved`() {
156156
val dataFetcher = FunctionDataFetcher(target = MyClass(), fn = MyClass::suspendThrow)
157157
val mockEnvironmet: DataFetchingEnvironment = mockk()
158-
every { mockEnvironmet.arguments } returns mapOf("value" to null)
159158

160159
try {
161160
val result = dataFetcher.get(mockEnvironmet)

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/extensions/DeepNameKtTest.kt renamed to graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/extensions/GraphqlTypeExtensionsKtTest.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import io.mockk.mockk
2424
import org.junit.jupiter.api.Test
2525
import kotlin.test.assertEquals
2626

27-
internal class DeepNameKtTest {
27+
internal class GraphqlTypeExtensionsKtTest {
2828

2929
private val basicType = mockk<GraphQLNamedType> {
3030
every { name } returns "BasicType"
@@ -52,4 +52,27 @@ internal class DeepNameKtTest {
5252
val complicated = GraphQLNonNull(GraphQLList(GraphQLNonNull(basicType)))
5353
assertEquals(expected = "[BasicType!]!", actual = complicated.deepName)
5454
}
55+
56+
@Test
57+
fun `unwrapType works on basic types that are not wrapped`() {
58+
assertEquals("BasicType", basicType.unwrapType().deepName)
59+
}
60+
61+
@Test
62+
fun `unwrapType works on non null`() {
63+
val nonNull = GraphQLNonNull.nonNull(basicType)
64+
assertEquals("BasicType", nonNull.unwrapType().deepName)
65+
}
66+
67+
@Test
68+
fun `unwrapType works on lists`() {
69+
val graphQLList = GraphQLList.list(basicType)
70+
assertEquals("BasicType", graphQLList.unwrapType().deepName)
71+
}
72+
73+
@Test
74+
fun `unwrapType works on multiple layers`() {
75+
val graphQLList = GraphQLNonNull.nonNull(GraphQLList.list(GraphQLNonNull.nonNull(basicType)))
76+
assertEquals("BasicType", graphQLList.unwrapType().deepName)
77+
}
5578
}

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/filters/SuperclassFiltersKtTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.expediagroup.graphql.generator.filters
1818

19+
import com.expediagroup.graphql.annotations.GraphQLIgnore
1920
import org.junit.jupiter.api.Test
2021
import kotlin.reflect.KClass
2122
import kotlin.test.assertFalse
@@ -35,12 +36,18 @@ class SuperclassFiltersKtTest {
3536
fun internal(): String
3637
}
3738

39+
@GraphQLIgnore
40+
interface IgnoredInterface {
41+
fun public(): String
42+
}
43+
3844
@Test
3945
fun superclassFilters() {
4046
assertTrue(isValidSuperclass(Interface::class))
4147
assertFalse(isValidSuperclass(Union::class))
4248
assertFalse(isValidSuperclass(NonPublic::class))
4349
assertFalse(isValidSuperclass(Class::class))
50+
assertFalse(isValidSuperclass(IgnoredInterface::class))
4451
}
4552

4653
private fun isValidSuperclass(kClass: KClass<*>): Boolean = superclassFilters.all { it(kClass) }

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooksTest.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@ import com.expediagroup.graphql.generator.extensions.getSimpleName
2525
import com.expediagroup.graphql.getTestSchemaConfigWithHooks
2626
import com.expediagroup.graphql.testSchemaConfig
2727
import com.expediagroup.graphql.toSchema
28+
import graphql.language.StringValue
29+
import graphql.schema.Coercing
2830
import graphql.schema.GraphQLFieldDefinition
2931
import graphql.schema.GraphQLInterfaceType
3032
import graphql.schema.GraphQLObjectType
33+
import graphql.schema.GraphQLScalarType
3134
import graphql.schema.GraphQLSchema
3235
import graphql.schema.GraphQLType
3336
import org.junit.jupiter.api.Test
3437
import org.junit.jupiter.api.assertThrows
38+
import java.util.UUID
3539
import kotlin.random.Random
3640
import kotlin.reflect.KClass
3741
import kotlin.reflect.KFunction
@@ -251,10 +255,40 @@ class SchemaGeneratorHooksTest {
251255
assertEquals(expected = "SomeData", actual = hooks.willResolveMonad(type).getSimpleName())
252256
}
253257

258+
@Test
259+
fun `willGenerateGraphQLType can override to provide a custom type`() {
260+
class MockSchemaGeneratorHooks : SchemaGeneratorHooks {
261+
var hookCalled = false
262+
263+
override fun willGenerateGraphQLType(type: KType): GraphQLType? {
264+
hookCalled = true
265+
266+
return when (type.classifier as? KClass<*>) {
267+
UUID::class -> graphqlUUIDType
268+
else -> null
269+
}
270+
}
271+
}
272+
273+
val hooks = MockSchemaGeneratorHooks()
274+
val schema = toSchema(
275+
queries = listOf(TopLevelObject(CustomTypesQuery())),
276+
config = getTestSchemaConfigWithHooks(hooks)
277+
)
278+
279+
assertTrue(hooks.hookCalled)
280+
val graphQLType = assertNotNull(schema.getType("UUID"))
281+
assertTrue(graphQLType is GraphQLScalarType)
282+
}
283+
254284
class TestQuery {
255285
fun query(): SomeData = SomeData("someData", 0)
256286
}
257287

288+
class CustomTypesQuery {
289+
fun uuid(): UUID = UUID.randomUUID()
290+
}
291+
258292
class TestInterfaceQuery {
259293
fun randomQuery(): RandomData = if (Random.nextBoolean()) {
260294
SomeData("random", 1)
@@ -294,4 +328,21 @@ class SchemaGeneratorHooksTest {
294328
}
295329

296330
class EmptyImplementation(override val id: String) : EmptyInterface
331+
332+
private val graphqlUUIDType = GraphQLScalarType.newScalar()
333+
.name("UUID")
334+
.description("A type representing a formatted java.util.UUID")
335+
.coercing(UUIDCoercing)
336+
.build()
337+
338+
private object UUIDCoercing : Coercing<UUID, String> {
339+
override fun parseValue(input: Any?): UUID = UUID.fromString(serialize(input))
340+
341+
override fun parseLiteral(input: Any?): UUID? {
342+
val uuidString = (input as? StringValue)?.value
343+
return UUID.fromString(uuidString)
344+
}
345+
346+
override fun serialize(dataFetcherResult: Any?): String = dataFetcherResult.toString()
347+
}
297348
}

0 commit comments

Comments
 (0)