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

Issue:
`extend input` fails to extend
Given this schema, the second definition overwrites the first rather than extending it.

```
input InputType {
  fieldA: String
}

extend input InputType {
  fieldB: String
}
```

Pull request:
Resolves #353

## 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

The GraphQL spec allows `extend input`, but the graphql-java-tools library did not handle this case.

This commit, enables the following to work:

```
type Mutation {
    save(input: UserInput!): Boolean
}

input UserInput {
    name: String                        
}

extend input UserInput {
    password: String
}

# and later on:
# mutation {
#    saveUser(input: {name: "John", password: "secret"})
# }
```

Patch:
diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
index a9bf173c..0b70bebe 100644
--- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
@@ -32,8 +32,9 @@ internal class SchemaClassScanner(
 
     private val initialDictionary = initialDictionary.mapValues { InitialDictionaryEntry(it.value) }
     private val extensionDefinitions = allDefinitions.filterIsInstance<ObjectTypeExtensionDefinition>()
+    private val inputExtensionDefinitions = allDefinitions.filterIsInstance<InputObjectTypeExtensionDefinition>()
 
-    private val definitionsByName = (allDefinitions.filterIsInstance<TypeDefinition<*>>() - extensionDefinitions).associateBy { it.name }
+    private val definitionsByName = (allDefinitions.filterIsInstance<TypeDefinition<*>>() - extensionDefinitions - inputExtensionDefinitions).associateBy { it.name }
     private val objectDefinitions = (allDefinitions.filterIsInstance<ObjectTypeDefinition>() - extensionDefinitions)
     private val objectDefinitionsByName = objectDefinitions.associateBy { it.name }
     private val interfaceDefinitionsByName = allDefinitions.filterIsInstance<InterfaceTypeDefinition>().associateBy { it.name }
@@ -173,7 +174,7 @@ internal class SchemaClassScanner(
         validateRootResolversWereUsed(rootTypeHolder.mutation, fieldResolvers)
         validateRootResolversWereUsed(rootTypeHolder.subscription, fieldResolvers)
 
-        return ScannedSchemaObjects(dictionary, observedDefinitions + extensionDefinitions, scalars, rootInfo, fieldResolversByType.toMap(), unusedDefinitions)
+        return ScannedSchemaObjects(dictionary, observedDefinitions + extensionDefinitions + inputExtensionDefinitions, scalars, rootInfo, fieldResolversByType.toMap(), unusedDefinitions)
     }
 
     private fun validateRootResolversWereUsed(rootType: RootType?, fieldResolvers: List<FieldResolver>) {
diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt
index 5ad7b6be..d0cd1298 100644
--- a/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt
@@ -45,9 +45,10 @@ class SchemaParser internal constructor(
     private val unusedDefinitions = scanResult.unusedDefinitions
 
     private val extensionDefinitions = definitions.filterIsInstance<ObjectTypeExtensionDefinition>()
+    private val inputExtensionDefinitions = definitions.filterIsInstance<InputObjectTypeExtensionDefinition>()
 
     private val objectDefinitions = (definitions.filterIsInstance<ObjectTypeDefinition>() - extensionDefinitions)
-    private val inputObjectDefinitions = definitions.filterIsInstance<InputObjectTypeDefinition>()
+    private val inputObjectDefinitions = (definitions.filterIsInstance<InputObjectTypeDefinition>() - inputExtensionDefinitions)
     private val enumDefinitions = definitions.filterIsInstance<EnumTypeDefinition>()
     private val interfaceDefinitions = definitions.filterIsInstance<InterfaceTypeDefinition>()
 
@@ -173,22 +174,27 @@ class SchemaParser internal constructor(
     }
 
     private fun createInputObject(definition: InputObjectTypeDefinition, inputObjects: List<GraphQLInputObjectType>): GraphQLInputObjectType {
+        val extensionDefinitions = inputExtensionDefinitions.filter { it.name == definition.name }
+
         val builder = GraphQLInputObjectType.newInputObject()
             .name(definition.name)
             .definition(definition)
+            .extensionDefinitions(extensionDefinitions)
             .description(if (definition.description != null) definition.description.content else getDocumentation(definition))
 
         builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.INPUT_OBJECT))
 
-        definition.inputValueDefinitions.forEach { inputDefinition ->
-            val fieldBuilder = GraphQLInputObjectField.newInputObjectField()
-                .name(inputDefinition.name)
-                .definition(inputDefinition)
-                .description(if (inputDefinition.description != null) inputDefinition.description.content else getDocumentation(inputDefinition))
-                .defaultValue(buildDefaultValue(inputDefinition.defaultValue))
-                .type(determineInputType(inputDefinition.type, inputObjects))
-                .withDirectives(*buildDirectives(inputDefinition.directives, setOf(), Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION))
-            builder.field(fieldBuilder.build())
+        (extensionDefinitions + definition).forEach {
+            it.inputValueDefinitions.forEach { inputDefinition ->
+                val fieldBuilder = GraphQLInputObjectField.newInputObjectField()
+                        .name(inputDefinition.name)
+                        .definition(inputDefinition)
+                        .description(if (inputDefinition.description != null) inputDefinition.description.content else getDocumentation(inputDefinition))
+                        .defaultValue(buildDefaultValue(inputDefinition.defaultValue))
+                        .type(determineInputType(inputDefinition.type, inputObjects))
+                        .withDirectives(*buildDirectives(inputDefinition.directives, setOf(), Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION))
+                builder.field(fieldBuilder.build())
+            }
         }
 
         return schemaGeneratorDirectiveHelper.onInputObjectType(builder.build(), schemaDirectiveParameters)


Test patch:
diff --git a/src/test/groovy/graphql/kickstart/tools/EndToEndSpec.groovy b/src/test/groovy/graphql/kickstart/tools/EndToEndSpec.groovy
index 7bbcc3d2..dc8f10e1 100644
--- a/src/test/groovy/graphql/kickstart/tools/EndToEndSpec.groovy
+++ b/src/test/groovy/graphql/kickstart/tools/EndToEndSpec.groovy
@@ -509,6 +509,20 @@ class EndToEndSpec extends Specification {
         ]
     }
 
+    def "generated schema should handle extended input types"() {
+        when:
+        def data = Utils.assertNoGraphQlErrors(gql) {
+            '''
+                mutation {
+                    saveUser(input: {name: "John", password: "secret"})
+                }
+                '''
+        }
+
+        then:
+        data.saveUser == "John/secret"
+    }
+
     def "generated schema supports generic properties"() {
         when:
         def data = Utils.assertNoGraphQlErrors(gql) {
diff --git a/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy b/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy
index 67f4fa8e..6b25f72b 100644
--- a/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy
+++ b/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy
@@ -2,6 +2,7 @@ package graphql.kickstart.tools
 
 import graphql.execution.batched.Batched
 import graphql.language.InputObjectTypeDefinition
+import graphql.language.InputObjectTypeExtensionDefinition
 import graphql.language.InterfaceTypeDefinition
 import graphql.language.ObjectTypeDefinition
 import graphql.language.ScalarTypeDefinition
@@ -159,6 +160,37 @@ class SchemaClassScannerSpec extends Specification {
         }
     }
 
+    def "scanner handles input types extensions"() {
+        when:
+        ScannedSchemaObjects objects = SchemaParser.newParser()
+                .schemaString('''
+                    type Query { }
+
+                    type Mutation {
+                        save(input: UserInput!): Boolean
+                    }
+                    
+                    input UserInput {
+                        name: String                        
+                    }
+                    
+                    extend input UserInput {
+                        password: String
+                    }
+                ''')
+                .resolvers(
+                        new GraphQLMutationResolver() {
+                            boolean save(Map map) { true }
+                        },
+                        new GraphQLQueryResolver() {}
+                )
+                .scan()
+
+        then:
+        objects.definitions.findAll { (it.class == InputObjectTypeExtensionDefinition.class) }.size() == 1
+        objects.definitions.findAll { (it.class == InputObjectTypeDefinition.class) }.size() == 1
+    }
+
     def "scanner allows multiple return types for custom scalars"() {
         when:
         ScannedSchemaObjects objects = SchemaParser.newParser()
diff --git a/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt b/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt
index 90ff2fbb..38d69d13 100644
--- a/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt
+++ b/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt
@@ -111,6 +111,7 @@ input ComplexInputTypeTwo {
 type Mutation {
     addItem(newItem: NewItemInput!): Item!
     echoFiles(fileParts: [Upload!]!): [String!]!
+    saveUser(input: UserInput!): String
 }
 
 type Subscription {
@@ -119,6 +120,14 @@ type Subscription {
     onItemCreatedCoroutineChannelAndSuspendFunction: Item!
 }
 
+input UserInput {
+    name: String                        
+}
+
+extend input UserInput {
+    password: String
+}
+
 input ItemSearchInput {
     # The item name to look for
     name: String!
@@ -321,6 +330,15 @@ class Mutation : GraphQLMutationResolver {
     fun addItem(input: NewItemInput): Item {
         return Item(items.size, input.name, input.type, UUID.randomUUID(), listOf()) // Don't actually add the item to the list, since we want the test to be deterministic
     }
+
+    fun saveUser(userInput: UserInput): String {
+        return userInput.name + "/" + userInput.password;
+    }
+
+    class UserInput {
+        var name: String = ""
+        var password: String = ""
+    }
 }
 
 class OnItemCreatedContext(val newItem: Item)