Skip to content

Commit 6f9b4b1

Browse files
authored
BREAKING CHANGE(generator): make dependency on ClassGraphClass optional (#1733)
### 📝 Description We are using `ClassScanner` (which wraps `ClassGraph`) to automatically resolve polymorphic types (i.e. given an interface we can find all implementing types that are available on classpath) and locate federated entities (i.e. types with `@key` directive). This is generally great as this information can be pulled in automatically but it is a blocker for using the library with GraalVM. `ClassGraph` is incompatible with GraalVm because when compiling to native image, we will no longer have JARs on classpath so any metadata around resources in those files will be invalid. Since we only use classpath scanning for resolving polyrmophic types and federated entities, the simplest solution is to provide users ability to manually provide this information without relying on the `ClassGraph` (which is still used by default). By no longer relying on `ClassGraph` we can now generate valid GraalVM reflect metadata and compile GraphQL Kotlin servers to native images. This PR introduces a breaking change that removes `SchemaGenerator#addAdditionalTypesWithAnnotation` function. This function was originally added to avoid exposing `ClassGraph` outside of `graphql-kotlin-schema-generator` module and allow federation module to easily locate entity types. By making `ClassGraph` dependency optional, function no longer makes sense. ### 🔗 Related Issues * #1543 * #1441 * #1224
1 parent d63620b commit 6f9b4b1

33 files changed

+272
-374
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2023 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.generator.federation
18+
19+
import com.expediagroup.graphql.generator.ClasspathTypeResolver
20+
import com.expediagroup.graphql.generator.GraphQLTypeResolver
21+
import com.expediagroup.graphql.generator.SimpleTypeResolver
22+
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
23+
import com.expediagroup.graphql.generator.internal.state.ClassScanner
24+
import kotlin.reflect.KClass
25+
26+
/**
27+
* Polymorphic GraphQL type resolver that can also locate federated entities.
28+
*/
29+
interface FederatedGraphQLTypeResolver : GraphQLTypeResolver {
30+
/**
31+
* Locate federated entities (i.e. types with `@key` directive)
32+
*/
33+
fun locateEntities(): List<KClass<*>>
34+
}
35+
36+
/**
37+
* Simple federated polymorphic type resolver that relies on provided polymorphic map and
38+
* list of entities.
39+
*/
40+
open class FederatedSimpleTypeResolver(
41+
polymorphicMap: Map<KClass<*>, List<KClass<*>>>,
42+
private val entities: List<KClass<*>>
43+
) : SimpleTypeResolver(polymorphicMap), FederatedGraphQLTypeResolver {
44+
override fun locateEntities(): List<KClass<*>> = entities
45+
}
46+
47+
/**
48+
* Classpath based federated polymorphic type resolver. Relies on ClassScanner to obtain
49+
* polymorphic info and to locate entities.
50+
*/
51+
open class FederatedClasspathTypeResolver(
52+
private val scanner: ClassScanner
53+
) : ClasspathTypeResolver(scanner), FederatedGraphQLTypeResolver {
54+
override fun locateEntities(): List<KClass<*>> = scanner.getClassesWithAnnotation(KeyDirective::class)
55+
}

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Expedia, Inc
2+
* Copyright 2023 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.
@@ -18,14 +18,16 @@ package com.expediagroup.graphql.generator.federation
1818

1919
import com.expediagroup.graphql.generator.SchemaGenerator
2020
import com.expediagroup.graphql.generator.TopLevelObject
21-
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
2221
import graphql.schema.GraphQLSchema
2322
import kotlin.reflect.KType
23+
import kotlin.reflect.full.createType
2424

2525
/**
2626
* Generates federated GraphQL schemas based on the specified configuration.
2727
*/
28-
open class FederatedSchemaGenerator(generatorConfig: FederatedSchemaGeneratorConfig) : SchemaGenerator(generatorConfig) {
28+
open class FederatedSchemaGenerator(
29+
private val generatorConfig: FederatedSchemaGeneratorConfig
30+
) : SchemaGenerator(generatorConfig) {
2931

3032
/**
3133
* Scans specified packages for all the federated (extended) types and adds them to the schema additional types,
@@ -39,7 +41,7 @@ open class FederatedSchemaGenerator(generatorConfig: FederatedSchemaGeneratorCon
3941
additionalInputTypes: Set<KType>,
4042
schemaObject: TopLevelObject?
4143
): GraphQLSchema {
42-
addAdditionalTypesWithAnnotation(KeyDirective::class, inputType = false)
43-
return super.generateSchema(queries, mutations, subscriptions, additionalTypes, additionalInputTypes, schemaObject)
44+
val entityTypes = generatorConfig.typeResolver.locateEntities().map { it.createType() }
45+
return super.generateSchema(queries, mutations, subscriptions, additionalTypes + entityTypes, additionalInputTypes, schemaObject)
4446
}
4547
}

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorConfig.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2023 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.
@@ -20,14 +20,19 @@ import com.expediagroup.graphql.generator.SchemaGeneratorConfig
2020
import com.expediagroup.graphql.generator.TopLevelNames
2121
import com.expediagroup.graphql.generator.execution.KotlinDataFetcherFactoryProvider
2222
import com.expediagroup.graphql.generator.execution.SimpleKotlinDataFetcherFactoryProvider
23+
import com.expediagroup.graphql.generator.internal.state.ClassScanner
24+
import graphql.schema.GraphQLType
2325

2426
/**
2527
* Settings for generating the federated schema.
2628
*/
29+
@Suppress("LongParameterList")
2730
class FederatedSchemaGeneratorConfig(
2831
override val supportedPackages: List<String>,
2932
override val topLevelNames: TopLevelNames = TopLevelNames(),
3033
override val hooks: FederatedSchemaGeneratorHooks,
3134
override val dataFetcherFactoryProvider: KotlinDataFetcherFactoryProvider = SimpleKotlinDataFetcherFactoryProvider(),
32-
override val introspectionEnabled: Boolean = true
33-
) : SchemaGeneratorConfig(supportedPackages, topLevelNames, hooks, dataFetcherFactoryProvider, introspectionEnabled)
35+
override val introspectionEnabled: Boolean = true,
36+
override val additionalTypes: Set<GraphQLType> = emptySet(),
37+
override val typeResolver: FederatedGraphQLTypeResolver = FederatedClasspathTypeResolver(ClassScanner(supportedPackages))
38+
) : SchemaGeneratorConfig(supportedPackages, topLevelNames, hooks, dataFetcherFactoryProvider, introspectionEnabled, additionalTypes, typeResolver)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2023 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.generator
18+
19+
import com.expediagroup.graphql.generator.internal.state.ClassScanner
20+
import java.io.Closeable
21+
import kotlin.reflect.KClass
22+
23+
/**
24+
* Polymorphic types resolver.
25+
*/
26+
interface GraphQLTypeResolver : Closeable {
27+
/**
28+
* Given a class find all its subtypes.
29+
*/
30+
fun getSubTypesOf(kclass: KClass<*>): List<KClass<*>>
31+
}
32+
33+
/**
34+
* Map based polymorphic types resolver.
35+
*/
36+
open class SimpleTypeResolver(
37+
private val polymorphicMap: Map<KClass<*>, List<KClass<*>>>
38+
) : GraphQLTypeResolver {
39+
40+
override fun getSubTypesOf(kclass: KClass<*>): List<KClass<*>> = polymorphicMap[kclass] ?: emptyList()
41+
42+
override fun close() {
43+
// no-op
44+
}
45+
}
46+
47+
/**
48+
* Classpath based polymorphic type resolvers. Uses ClassScanner to find polymorphic info.
49+
*/
50+
open class ClasspathTypeResolver(
51+
private val scanner: ClassScanner
52+
) : GraphQLTypeResolver {
53+
override fun getSubTypesOf(kclass: KClass<*>): List<KClass<*>> = scanner.getSubTypesOf(kclass)
54+
55+
override fun close() {
56+
scanner.close()
57+
}
58+
}

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

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Expedia, Inc
2+
* Copyright 2023 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.
@@ -16,11 +16,9 @@
1616

1717
package com.expediagroup.graphql.generator
1818

19-
import com.expediagroup.graphql.generator.exceptions.InvalidPackagesException
2019
import com.expediagroup.graphql.generator.internal.extensions.getGraphQLDescription
2120
import com.expediagroup.graphql.generator.internal.extensions.getKClass
2221
import com.expediagroup.graphql.generator.internal.state.AdditionalType
23-
import com.expediagroup.graphql.generator.internal.state.ClassScanner
2422
import com.expediagroup.graphql.generator.internal.state.TypesCache
2523
import com.expediagroup.graphql.generator.internal.types.GraphQLKTypeMetadata
2624
import com.expediagroup.graphql.generator.internal.types.generateGraphQLType
@@ -36,9 +34,7 @@ import graphql.schema.GraphQLTypeUtil
3634
import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility
3735
import java.io.Closeable
3836
import java.util.concurrent.ConcurrentHashMap
39-
import kotlin.reflect.KClass
4037
import kotlin.reflect.KType
41-
import kotlin.reflect.full.createType
4238

4339
/**
4440
* Generate a schema object given some configuration and top level objects for the queries, mutations, and subscriptions.
@@ -52,20 +48,10 @@ import kotlin.reflect.full.createType
5248
open class SchemaGenerator(internal val config: SchemaGeneratorConfig) : Closeable {
5349

5450
internal val additionalTypes: MutableSet<AdditionalType> = mutableSetOf()
55-
internal val classScanner = ClassScanner(config.supportedPackages)
5651
internal val cache = TypesCache(config.supportedPackages)
57-
internal val codeRegistry = GraphQLCodeRegistry.newCodeRegistry()
52+
internal val codeRegistry: GraphQLCodeRegistry.Builder = GraphQLCodeRegistry.newCodeRegistry()
5853
internal val directives = ConcurrentHashMap<String, GraphQLDirective>()
5954

60-
/**
61-
* Validate that the supported packages contain classes
62-
*/
63-
init {
64-
if (classScanner.isEmptyScan()) {
65-
throw InvalidPackagesException(config.supportedPackages)
66-
}
67-
}
68-
6955
/**
7056
* Generate a schema given a list of objects to parse for the queries, mutations, and subscriptions.
7157
*/
@@ -77,7 +63,6 @@ open class SchemaGenerator(internal val config: SchemaGeneratorConfig) : Closeab
7763
additionalInputTypes: Set<KType> = emptySet(),
7864
schemaObject: TopLevelObject? = null
7965
): GraphQLSchema {
80-
8166
this.additionalTypes.addAll(additionalTypes.map { AdditionalType(it, inputType = false) })
8267
this.additionalTypes.addAll(additionalInputTypes.map { AdditionalType(it, inputType = true) })
8368

@@ -106,19 +91,6 @@ open class SchemaGenerator(internal val config: SchemaGeneratorConfig) : Closeab
10691
}.build()
10792
}
10893

109-
/**
110-
* Add all types with the following annotation to the schema.
111-
*
112-
* This is helpful for things like federation or combining external schemas.
113-
*/
114-
protected fun addAdditionalTypesWithAnnotation(annotation: KClass<*>, inputType: Boolean = false) {
115-
classScanner.getClassesWithAnnotation(annotation).forEach { kClass ->
116-
if (config.hooks.isValidAdditionalType(kClass, inputType)) {
117-
additionalTypes.add(AdditionalType(kClass.createType(), inputType))
118-
}
119-
}
120-
}
121-
12294
/**
12395
* Generate the GraphQL type for all the `additionalTypes`.
12496
*
@@ -127,7 +99,7 @@ open class SchemaGenerator(internal val config: SchemaGeneratorConfig) : Closeab
12799
*
128100
* This function loops because while generating the additionalTypes it is possible to create more additional types that need to be processed.
129101
*/
130-
protected fun generateAdditionalTypes(): Set<GraphQLType> {
102+
internal fun generateAdditionalTypes(): Set<GraphQLType> {
131103
val graphqlTypes = this.config.additionalTypes.toMutableSet()
132104
while (this.additionalTypes.isNotEmpty()) {
133105
val currentlyProcessedTypes = LinkedHashSet(this.additionalTypes)
@@ -152,7 +124,7 @@ open class SchemaGenerator(internal val config: SchemaGeneratorConfig) : Closeab
152124
* clean up of resources for you.
153125
*/
154126
override fun close() {
155-
classScanner.close()
127+
config.close()
156128
cache.close()
157129
additionalTypes.clear()
158130
directives.clear()

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2023 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.
@@ -20,16 +20,24 @@ import com.expediagroup.graphql.generator.execution.KotlinDataFetcherFactoryProv
2020
import com.expediagroup.graphql.generator.execution.SimpleKotlinDataFetcherFactoryProvider
2121
import com.expediagroup.graphql.generator.hooks.NoopSchemaGeneratorHooks
2222
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
23+
import com.expediagroup.graphql.generator.internal.state.ClassScanner
2324
import graphql.schema.GraphQLType
25+
import java.io.Closeable
2426

2527
/**
2628
* Settings for generating the schema.
2729
*/
30+
@Suppress("LongParameterList")
2831
open class SchemaGeneratorConfig(
2932
open val supportedPackages: List<String>,
3033
open val topLevelNames: TopLevelNames = TopLevelNames(),
3134
open val hooks: SchemaGeneratorHooks = NoopSchemaGeneratorHooks,
3235
open val dataFetcherFactoryProvider: KotlinDataFetcherFactoryProvider = SimpleKotlinDataFetcherFactoryProvider(),
3336
open val introspectionEnabled: Boolean = true,
34-
open val additionalTypes: Set<GraphQLType> = emptySet()
35-
)
37+
open val additionalTypes: Set<GraphQLType> = emptySet(),
38+
open val typeResolver: GraphQLTypeResolver = ClasspathTypeResolver(ClassScanner(supportedPackages))
39+
) : Closeable {
40+
override fun close() {
41+
typeResolver.close()
42+
}
43+
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.expediagroup.graphql.generator.internal.state
1818

19+
import com.expediagroup.graphql.generator.exceptions.InvalidPackagesException
1920
import io.github.classgraph.ClassGraph
2021
import io.github.classgraph.ClassInfo
2122
import io.github.classgraph.ClassInfoList
@@ -27,14 +28,23 @@ import kotlin.reflect.jvm.jvmName
2728
* This class should be used from a try-with-resouces block
2829
* or another closable object as the internal scan result can take up a lot of resources.
2930
*/
30-
internal class ClassScanner(supportedPackages: List<String>) : Closeable {
31+
open class ClassScanner(supportedPackages: List<String>) : Closeable {
3132

3233
@Suppress("Detekt.SpreadOperator")
3334
private val scanResult = ClassGraph()
3435
.enableAllInfo()
3536
.acceptPackages(*supportedPackages.toTypedArray())
3637
.scan()
3738

39+
/**
40+
* Validate that the supported packages contain classes
41+
*/
42+
init {
43+
if (isEmptyScan()) {
44+
throw InvalidPackagesException(supportedPackages)
45+
}
46+
}
47+
3848
/**
3949
* Return true if there are no valid packages scanned
4050
*/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ internal fun generateInterface(generator: SchemaGenerator, kClass: KClass<*>): G
6060
kClass.getValidFunctions(generator.config.hooks)
6161
.forEach { builder.field(generateFunction(generator, kClass, it, kClass.getSimpleName(), null, abstract = true)) }
6262

63-
generator.classScanner.getSubTypesOf(kClass)
63+
generator.config.typeResolver.getSubTypesOf(kClass)
6464
.filter { generator.config.hooks.isValidAdditionalType(it, inputType = false) }
6565
.forEach { generator.additionalTypes.add(AdditionalType(it.createType(), inputType = false)) }
6666

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Expedia, Inc
2+
* Copyright 2023 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.
@@ -71,7 +71,7 @@ private fun generateUnionFromKClass(generator: SchemaGenerator, kClass: KClass<*
7171
builder.withAppliedDirective(it)
7272
}
7373

74-
val types = generator.classScanner.getSubTypesOf(kClass)
74+
val types = generator.config.typeResolver.getSubTypesOf(kClass)
7575

7676
return createUnion(name, generator, builder, types)
7777
}

0 commit comments

Comments
 (0)