diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index 860689c5..b45ada00 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -77,13 +77,30 @@ internal class SchemaClassScanner( do { do { // Require all implementors of discovered interfaces to be discovered or provided. - handleInterfaceOrUnionSubTypes(getAllObjectTypesImplementingDiscoveredInterfaces()) { "Object type '${it.name}' implements a known interface, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } + handleDictionaryTypes(getAllObjectTypesImplementingDiscoveredInterfaces()) { "Object type '${it.name}' implements a known interface, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } } while (scanQueue()) // Require all members of discovered unions to be discovered. - handleInterfaceOrUnionSubTypes(getAllObjectTypeMembersOfDiscoveredUnions()) { "Object type '${it.name}' is a member of a known union, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } + handleDictionaryTypes(getAllObjectTypeMembersOfDiscoveredUnions()) { "Object type '${it.name}' is a member of a known union, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } } while (scanQueue()) + // Find unused types and include them if required + if (options.includeUnusedTypes) { + do { + val unusedDefinitions = (definitionsByName.values - (dictionary.keys.toSet() + unvalidatedTypes)) + .filter { definition -> definition.name != "PageInfo" } + .filterIsInstance().distinct() + + if (unusedDefinitions.isEmpty()) { + break + } + + val unusedDefinition = unusedDefinitions.first() + + handleDictionaryTypes(listOf(unusedDefinition)) { "Object type '${it.name}' is unused and includeUnusedTypes is true. Please pass a class for type '${it.name}' in the parser's dictionary." } + } while (scanQueue()) + } + return validateAndCreateResult(rootTypeHolder) } @@ -208,7 +225,7 @@ internal class SchemaClassScanner( }.flatten().distinct() } - private fun handleInterfaceOrUnionSubTypes(types: List, failureMessage: (ObjectTypeDefinition) -> String) { + private fun handleDictionaryTypes(types: List, failureMessage: (ObjectTypeDefinition) -> String) { types.forEach { type -> val dictionaryContainsType = dictionary.filter { it.key.name == type.name }.isNotEmpty() if (!unvalidatedTypes.contains(type) && !dictionaryContainsType) { diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt index f64b1d46..411236e0 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt @@ -29,7 +29,8 @@ data class SchemaParserOptions internal constructor( val introspectionEnabled: Boolean, val coroutineContextProvider: CoroutineContextProvider, val typeDefinitionFactories: List, - val fieldVisibility: GraphqlFieldVisibility? + val fieldVisibility: GraphqlFieldVisibility?, + val includeUnusedTypes: Boolean ) { companion object { @JvmStatic @@ -56,6 +57,7 @@ data class SchemaParserOptions internal constructor( private var coroutineContextProvider: CoroutineContextProvider? = null private var typeDefinitionFactories: MutableList = mutableListOf(RelayConnectionFactory()) private var fieldVisibility: GraphqlFieldVisibility? = null + private var includeUnusedTypes = false fun contextClass(contextClass: Class<*>) = this.apply { this.contextClass = contextClass @@ -125,6 +127,10 @@ data class SchemaParserOptions internal constructor( this.fieldVisibility = fieldVisibility } + fun includeUnusedTypes(includeUnusedTypes: Boolean) = this.apply { + this.includeUnusedTypes = includeUnusedTypes + } + @ExperimentalCoroutinesApi fun build(): SchemaParserOptions { val coroutineContextProvider = coroutineContextProvider @@ -162,7 +168,8 @@ data class SchemaParserOptions internal constructor( introspectionEnabled, coroutineContextProvider, typeDefinitionFactories, - fieldVisibility + fieldVisibility, + includeUnusedTypes ) } } diff --git a/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy b/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy index ca7d652c..876ba3c6 100644 --- a/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy +++ b/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy @@ -1,10 +1,6 @@ package graphql.kickstart.tools -import graphql.language.InputObjectTypeDefinition -import graphql.language.InputObjectTypeExtensionDefinition -import graphql.language.InterfaceTypeDefinition -import graphql.language.ObjectTypeDefinition -import graphql.language.ScalarTypeDefinition +import graphql.language.* import graphql.schema.Coercing import graphql.schema.GraphQLScalarType import spock.lang.Specification @@ -409,4 +405,108 @@ class SchemaClassScannerSpec extends Specification { String id } } + + def "scanner should handle unused types when option is true"() { + when: + ScannedSchemaObjects objects = SchemaParser.newParser() + .schemaString(''' + # Let's say this is the Products service from Apollo Federation Introduction + + type Query { + allProducts: [Product] + } + + type Product { + name: String + } + + # these directives are defined in the Apollo Federation Specification: + # https://www.apollographql.com/docs/apollo-server/federation/federation-spec/ + type User @key(fields: "id") @extends { + id: ID! @external + recentPurchasedProducts: [Product] + address: Address + } + + type Address { + street: String + } + ''') + .resolvers(new GraphQLQueryResolver() { + List allProducts() { null } + }) + .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) + .dictionary(User) + .scan() + + then: + objects.definitions.find { it.name == "User" } != null + objects.definitions.find { it.name == "Address" } != null + } + + class Product { + String name + } + + class User { + String id + List recentPurchasedProducts + Address address + } + + class Address { + String street + } + + def "scanner should handle unused types with interfaces when option is true"() { + when: + ScannedSchemaObjects objects = SchemaParser.newParser() + .schemaString(''' + type Query { + whatever: Whatever + } + + type Whatever { + value: String + } + + type Unused { + someInterface: SomeInterface + } + + interface SomeInterface { + value: String + } + + type Implementation implements SomeInterface { + value: String + } + ''') + .resolvers(new GraphQLQueryResolver() { + Whatever whatever() { null } + }) + .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) + .dictionary(Unused, Implementation) + .scan() + + then: + objects.definitions.find { it.name == "Unused" } != null + objects.definitions.find { it.name == "SomeInterface" } != null + objects.definitions.find { it.name == "Implementation" } != null + } + + class Whatever { + String value + } + + class Unused { + SomeInterface someInterface + } + + class Implementation implements SomeInterface { + @Override + String getValue() { + return null + } + } } diff --git a/src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy b/src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy index 4ee9b896..496dab49 100644 --- a/src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy +++ b/src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy @@ -9,3 +9,5 @@ interface Vehicle { } interface VehicleInformation {} + +interface SomeInterface { String getValue() }