Skip to content

Commit 997c392

Browse files
authored
BREAKING CHANGE: Refactor graphQLTypeOf to top level function (#535)
* BREAKING CHANGE: Refactor schema generator functions to top level file functions Move the functions to generate a graphql type and generate the schema to top level file functions. This makes these functions pure since they accept the state of the SchemaGenerator as an argument and cleans up the methods that we need to expose in the class. It does however still make these public in graphql-kotlin-schema-generator as we still need to be able to call them from graphql-kotlin-federation * Make generateGraphQLType internal * Remove redundant javadoc comments * Move internal files into correct packages * Move generateSchema into the SchemaGenerator * Undo copyright update * Move generateSchema into SchemaGenerator * Move addExtendedTypes back to FederatedSchemaGenerator * Move the add extended types into generateSchema Remove the uneeded extra method in FederatedSchemaGenerator in favor of overriding generateSchema with the additional behaviour of adding all the types annotated with the ExtendsDirective before generating the schema
1 parent 9e75a2e commit 997c392

28 files changed

+783
-745
lines changed

graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGenerator.kt

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,41 +20,18 @@ import com.expediagroup.graphql.TopLevelObject
2020
import com.expediagroup.graphql.federation.directives.ExtendsDirective
2121
import com.expediagroup.graphql.generator.SchemaGenerator
2222
import graphql.schema.GraphQLSchema
23-
import io.github.classgraph.ClassGraph
24-
import kotlin.reflect.full.starProjectedType
25-
import kotlin.reflect.jvm.jvmName
2623

2724
/**
2825
* Generates federated GraphQL schemas based on the specified configuration.
2926
*/
30-
open class FederatedSchemaGenerator(generatorConfig: FederatedSchemaGeneratorConfig) : SchemaGenerator(generatorConfig) {
31-
32-
override fun generate(
33-
queries: List<TopLevelObject>,
34-
mutations: List<TopLevelObject>,
35-
subscriptions: List<TopLevelObject>,
36-
builder: GraphQLSchema.Builder
37-
): GraphQLSchema {
38-
builder.federation(config.supportedPackages)
39-
return super.generate(queries, mutations, subscriptions, builder)
40-
}
27+
class FederatedSchemaGenerator(generatorConfig: FederatedSchemaGeneratorConfig) : SchemaGenerator(generatorConfig) {
4128

4229
/**
43-
* Scans specified packages for all the federated (extended) types and adds them to the target schema before generating the rest of the schema
30+
* Scans specified packages for all the federated (extended) types and adds them to the schema additional types,
31+
* then it generates the schema as usual using the [FederatedSchemaGeneratorConfig].
4432
*/
45-
@Suppress("Detekt.SpreadOperator")
46-
private fun GraphQLSchema.Builder.federation(supportedPackages: List<String>): GraphQLSchema.Builder {
47-
val scanResult = ClassGraph().enableAllInfo().whitelistPackages(*supportedPackages.toTypedArray()).scan()
48-
49-
scanResult.getClassesWithAnnotation(ExtendsDirective::class.jvmName)
50-
.map { it.loadClass().kotlin }
51-
.map { graphQLTypeOf(it.starProjectedType, inputType = false, annotatedAsID = false) }
52-
.forEach {
53-
this.additionalType(it)
54-
}
55-
56-
scanResult.close()
57-
58-
return this
33+
override fun generateSchema(queries: List<TopLevelObject>, mutations: List<TopLevelObject>, subscriptions: List<TopLevelObject>): GraphQLSchema {
34+
addAdditionalTypesWithAnnotation(ExtendsDirective::class)
35+
return super.generateSchema(queries, mutations, subscriptions)
5936
}
6037
}

graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/toFederatedSchema.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 Expedia, Inc
2+
* Copyright 2019 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,5 +38,5 @@ fun toFederatedSchema(
3838
subscriptions: List<TopLevelObject> = emptyList()
3939
): GraphQLSchema {
4040
val generator = FederatedSchemaGenerator(config)
41-
return generator.generate(queries, mutations, subscriptions)
41+
return generator.generateSchema(queries, mutations, subscriptions)
4242
}

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,3 @@ open class SchemaGeneratorConfig(
3030
open val hooks: SchemaGeneratorHooks = NoopSchemaGeneratorHooks,
3131
open val dataFetcherFactoryProvider: KotlinDataFetcherFactoryProvider = SimpleKotlinDataFetcherFactoryProvider()
3232
)
33-
34-
/**
35-
* The names of the top level objects in the schema.
36-
*/
37-
data class TopLevelNames(
38-
val query: String = "Query",
39-
val mutation: String = "Mutation",
40-
val subscription: String = "Subscription"
41-
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql
18+
19+
/**
20+
* The names of the top level objects in the schema.
21+
*/
22+
data class TopLevelNames(
23+
val query: String = "Query",
24+
val mutation: String = "Mutation",
25+
val subscription: String = "Subscription"
26+
)

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

Lines changed: 51 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -18,100 +18,86 @@ package com.expediagroup.graphql.generator
1818

1919
import com.expediagroup.graphql.SchemaGeneratorConfig
2020
import com.expediagroup.graphql.TopLevelObject
21-
import com.expediagroup.graphql.generator.extensions.getKClass
22-
import com.expediagroup.graphql.generator.extensions.isEnum
23-
import com.expediagroup.graphql.generator.extensions.isInterface
24-
import com.expediagroup.graphql.generator.extensions.isListType
25-
import com.expediagroup.graphql.generator.extensions.isUnion
26-
import com.expediagroup.graphql.generator.extensions.wrapInNonNull
27-
import com.expediagroup.graphql.generator.state.SchemaGeneratorState
28-
import com.expediagroup.graphql.generator.state.TypesCacheKey
29-
import com.expediagroup.graphql.generator.types.generateEnum
30-
import com.expediagroup.graphql.generator.types.generateInputObject
31-
import com.expediagroup.graphql.generator.types.generateInterface
32-
import com.expediagroup.graphql.generator.types.generateList
21+
import com.expediagroup.graphql.directives.DeprecatedDirective
22+
import com.expediagroup.graphql.generator.state.ClassScanner
23+
import com.expediagroup.graphql.generator.state.TypesCache
24+
import com.expediagroup.graphql.generator.types.generateGraphQLType
3325
import com.expediagroup.graphql.generator.types.generateMutations
34-
import com.expediagroup.graphql.generator.types.generateObject
3526
import com.expediagroup.graphql.generator.types.generateQueries
36-
import com.expediagroup.graphql.generator.types.generateScalar
3727
import com.expediagroup.graphql.generator.types.generateSubscriptions
38-
import com.expediagroup.graphql.generator.types.generateUnion
28+
import graphql.Directives
3929
import graphql.schema.GraphQLCodeRegistry
30+
import graphql.schema.GraphQLDirective
4031
import graphql.schema.GraphQLSchema
4132
import graphql.schema.GraphQLType
42-
import graphql.schema.GraphQLTypeReference
43-
import graphql.schema.GraphQLTypeUtil
33+
import java.util.concurrent.ConcurrentHashMap
4434
import kotlin.reflect.KClass
45-
import kotlin.reflect.KType
35+
import kotlin.reflect.full.createType
4636

47-
open class SchemaGenerator(val config: SchemaGeneratorConfig) {
37+
/**
38+
* Generate a schema object given some configuration and top level objects for the queries, mutaitons, and subscriptions.
39+
*
40+
* This class maintains the state of the schema while generation is taking place. It is passed into the internal functions
41+
* so they can use the cache and add additional types and directives into the schema as they parse the Kotlin code.
42+
*/
43+
open class SchemaGenerator(internal val config: SchemaGeneratorConfig) {
4844

49-
internal val state = SchemaGeneratorState(config.supportedPackages)
50-
internal val subTypeMapper = SubTypeMapper(config.supportedPackages)
45+
internal val classScanner = ClassScanner(config.supportedPackages)
46+
internal val cache = TypesCache(config.supportedPackages)
5147
internal val codeRegistry = GraphQLCodeRegistry.newCodeRegistry()
48+
internal val additionalTypes = mutableSetOf<GraphQLType>()
49+
internal val directives = ConcurrentHashMap<String, GraphQLDirective>()
50+
51+
init {
52+
// NOTE: @include and @defer query directives are added by graphql-java by default
53+
// adding them explicitly here to keep it consistent with missing deprecated directive
54+
directives[Directives.IncludeDirective.name] = Directives.IncludeDirective
55+
directives[Directives.SkipDirective.name] = Directives.SkipDirective
56+
57+
// graphql-kotlin default directives
58+
// @deprecated directive is a built-in directive that each GraphQL server should provide bu currently it is not added by graphql-java
59+
// see https://github.com/graphql-java/graphql-java/issues/1598
60+
directives[DeprecatedDirective.name] = DeprecatedDirective
61+
}
5262

53-
open fun generate(
63+
/**
64+
* Generate a schema given a list of objects to parse for the queries, mutations, and subscriptions.
65+
*/
66+
open fun generateSchema(
5467
queries: List<TopLevelObject>,
5568
mutations: List<TopLevelObject> = emptyList(),
56-
subscriptions: List<TopLevelObject> = emptyList(),
57-
builder: GraphQLSchema.Builder = GraphQLSchema.newSchema()
69+
subscriptions: List<TopLevelObject> = emptyList()
5870
): GraphQLSchema {
71+
val builder = GraphQLSchema.newSchema()
5972
builder.query(generateQueries(this, queries))
6073
builder.mutation(generateMutations(this, mutations))
6174
builder.subscription(generateSubscriptions(this, subscriptions))
6275

6376
// add unreferenced interface implementations
64-
state.additionalTypes.forEach {
77+
additionalTypes.forEach {
6578
builder.additionalType(it)
6679
}
6780

68-
builder.additionalDirectives(state.directives.values.toSet())
81+
builder.additionalDirectives(directives.values.toSet())
6982
builder.codeRegistry(codeRegistry.build())
7083

7184
val schema = config.hooks.willBuildSchema(builder).build()
7285

73-
// Clean up the classpath scanner
74-
subTypeMapper.close()
86+
classScanner.close()
7587

7688
return schema
7789
}
7890

79-
open fun graphQLTypeOf(type: KType, inputType: Boolean = false, annotatedAsID: Boolean = false): GraphQLType {
80-
val hookGraphQLType = config.hooks.willGenerateGraphQLType(type)
81-
val graphQLType = hookGraphQLType
82-
?: generateScalar(this, type, annotatedAsID)
83-
?: objectFromReflection(type, inputType)
84-
85-
// Do not call the hook on GraphQLTypeReference as we have not generated the type yet
86-
val unwrappedType = GraphQLTypeUtil.unwrapType(graphQLType).lastElement()
87-
val typeWithNullability = graphQLType.wrapInNonNull(type)
88-
if (unwrappedType !is GraphQLTypeReference) {
89-
return config.hooks.didGenerateGraphQLType(type, typeWithNullability)
90-
}
91-
92-
return typeWithNullability
93-
}
94-
95-
private fun objectFromReflection(type: KType, inputType: Boolean): GraphQLType {
96-
val cacheKey = TypesCacheKey(type, inputType)
97-
val cachedType = state.cache.get(cacheKey)
98-
99-
if (cachedType != null) {
100-
return cachedType
101-
}
102-
103-
val kClass = type.getKClass()
104-
val graphQLType = state.cache.buildIfNotUnderConstruction(kClass, inputType) { getGraphQLType(kClass, inputType, type) }
105-
106-
return config.hooks.willAddGraphQLTypeToSchema(type, graphQLType)
107-
}
108-
109-
private fun getGraphQLType(kClass: KClass<*>, inputType: Boolean, type: KType): GraphQLType = when {
110-
kClass.isEnum() -> @Suppress("UNCHECKED_CAST") (generateEnum(this, kClass as KClass<Enum<*>>))
111-
kClass.isListType() -> generateList(this, type, inputType)
112-
kClass.isUnion() -> generateUnion(this, kClass)
113-
kClass.isInterface() -> generateInterface(this, kClass)
114-
inputType -> generateInputObject(this, kClass)
115-
else -> generateObject(this, kClass)
91+
/**
92+
* Add all types with the following annotation to the schema.
93+
*
94+
* This is helpful for things like federation or combining external schemas
95+
*/
96+
protected fun addAdditionalTypesWithAnnotation(annotation: KClass<*>) {
97+
classScanner.getClassesWithAnnotation(annotation)
98+
.map { generateGraphQLType(this, it.createType()) }
99+
.forEach {
100+
additionalTypes.add(it)
101+
}
116102
}
117103
}

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/SubTypeMapper.kt renamed to graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/state/ClassScanner.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.generator
17+
package com.expediagroup.graphql.generator.state
1818

1919
import io.github.classgraph.ClassGraph
2020
import io.github.classgraph.ClassInfo
2121
import io.github.classgraph.ClassInfoList
2222
import kotlin.reflect.KClass
2323
import kotlin.reflect.jvm.jvmName
2424

25-
internal class SubTypeMapper(supportedPackages: List<String>) {
25+
internal class ClassScanner(supportedPackages: List<String>) {
2626

2727
@Suppress("Detekt.SpreadOperator")
2828
private val scanResult = ClassGraph()
@@ -39,6 +39,8 @@ internal class SubTypeMapper(supportedPackages: List<String>) {
3939
.filterNot { it.isAbstract }
4040
}
4141

42+
fun getClassesWithAnnotation(annotation: KClass<*>) = scanResult.getClassesWithAnnotation(annotation.jvmName).map { it.loadClass().kotlin }
43+
4244
fun close() = scanResult.close()
4345

4446
@Suppress("Detekt.SwallowedException")

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

Lines changed: 0 additions & 42 deletions
This file was deleted.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal fun generateArgument(generator: SchemaGenerator, parameter: KParameter)
3333
throw InvalidInputFieldTypeException(parameter)
3434
}
3535

36-
val graphQLType = generator.graphQLTypeOf(parameter.type, inputType = true, annotatedAsID = parameter.isGraphQLID())
36+
val graphQLType = generateGraphQLType(generator, parameter.type, inputType = true, annotatedAsID = parameter.isGraphQLID())
3737

3838
// Deprecation of arguments is currently unsupported: https://github.com/facebook/graphql/issues/197
3939
val builder = GraphQLArgument.newArgument()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ internal fun generateFieldDirectives(generator: SchemaGenerator, field: Field):
4747

4848
private fun getDirective(generator: SchemaGenerator, directiveInfo: DirectiveInfo): GraphQLDirective {
4949
val directiveName = directiveInfo.effectiveName
50-
val directive = generator.state.directives.computeIfAbsent(directiveName) {
50+
val directive = generator.directives.computeIfAbsent(directiveName) {
5151
val builder = GraphQLDirective.newDirective()
5252
.name(directiveInfo.effectiveName)
5353
.description(directiveInfo.directiveAnnotation.description)
@@ -60,7 +60,7 @@ private fun getDirective(generator: SchemaGenerator, directiveInfo: DirectiveInf
6060
directiveClass.getValidProperties(generator.config.hooks).forEach { prop ->
6161
val propertyName = prop.name
6262
val value = prop.call(directiveInfo.directive)
63-
val type = generator.graphQLTypeOf(prop.returnType)
63+
val type = generateGraphQLType(generator, prop.returnType)
6464

6565
val argument = GraphQLArgument.newArgument()
6666
.name(propertyName)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ internal fun generateFunction(generator: SchemaGenerator, fn: KFunction<*>, pare
5050

5151
val typeFromHooks = generator.config.hooks.willResolveMonad(fn.returnType)
5252
val returnType = getWrappedReturnType(typeFromHooks)
53-
val graphQLOutputType = generator.graphQLTypeOf(returnType).safeCast<GraphQLOutputType>()
53+
val graphQLOutputType = generateGraphQLType(generator, returnType).safeCast<GraphQLOutputType>()
5454
val graphQLType = builder.type(graphQLOutputType).build()
5555
val coordinates = FieldCoordinates.coordinates(parentName, functionName)
5656

0 commit comments

Comments
 (0)