Repository: graphql-java-kickstart/graphql-java-tools (kotlin)

Issue:
Unused type definitions are not visible in introspection queries
If we define a type that is unused in the schema file e.g

```graphql
type User {
    ...
}

type Context {
    ...
}

type Query {
    user(id: ID!): User
}
```

- The `Query` and `User` types are returned from introspection queries but not `Context`. Is this by design?
- If it is by design, is there a way for the type to appear in introspection queries? I can see they appear in `SchemaParser.getUnusedDefinitions()` but is there a way to wire that in so that they are visible in introspection queries?

Pull request:
<!-- Uncomment one of the following lines as necessary. -->
Resolves #442
Resolves #420 

## Checklist
- [x] Pull requests follows the [contribution guide](https://github.com/graphql-java-kickstart/graphql-java-tools/wiki/Contribution-guide)
- [x] New or modified functionality is covered by tests

## Description
Adding a new `includeUnusedTypes` option to include unused types to the final executable schema. The caveat is that the unused types need to be included in the schema parser dictionary. I think it is an ok tradeoff as this feature has specific use cases as described in #420 and #442.

Added a couple of test cases to it as well as serve as both test and example of how to use it.

Patch:
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<ObjectTypeDefinition>().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<ObjectTypeDefinition>, failureMessage: (ObjectTypeDefinition) -> String) {
+    private fun handleDictionaryTypes(types: List<ObjectTypeDefinition>, 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<TypeDefinitionFactory>,
-    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<TypeDefinitionFactory> = 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
             )
         }
     }


Test patch:
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<Product> 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<Product> 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() }