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

Issue:
Support for a catch-all field resolver
In my schema I support object synthetic properties via a **compute(expression: String!)** method which does its job allowing me to return to a query any number of results. GraphQL specification allows renaming them in the result set by specifying aliases in the query:
```javascript
positions
{
    house_shorts
    house_longs
    nominal_short: compute(expression: "self.house_shorts * self.price")
    nominal_long: compute(expression: "self.house_longs * self.price")
}
```
This is all good but the problem is that I would like to be able to reference these properties further away in the query:
```
stream
{
    positions
    {
        ...
        nominal_short(expression: "self.house_shorts * self.price)
        nominal_long(expression: "self.house_longs * self.price")
    }
    filter(expression: "self.positions.nominal_short > 1000000 or self.positions.nominal_long > 1000000")
    {
        list
        {
            nominal_short
            nominal_long
        }
    }
}
```
In the latter query **positions, nominal_** are properties created at runtime and they cannot be resolved by the current implementation of this library. However, if the library would have a MissingFieldResolver which instead of throwing an exception would simply allow for calling a user defined handler things could work out differently. The handler can use a Map to associate "missing" properties with their values. A type resolver could implement a method, tag it with an annotation @GenericFieldResolver and return the value from it.

Pull request:
Resolves #129

## 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
This pull request adds support for custom missing field handling which allows the ability to have a catch-all field resolver implementation.

This feature is needed for our project to allow similar functionality to [GraphQL-Faker](https://github.com/APIs-guru/graphql-faker) where we want to return fake values for our clients teams to be able to experiment with our service schema.

To add custom missing resolver data fetcher you need to implement the following `DataFetcher<Any?>` interface:
```kotlin
class ExampleMissingResolverDataFetcher : DataFetcher<Any?> {
  override fun get(env: DataFetchingEnvironment?): Any? {
    return env?.getArgument("input")
  }
}
```

When building `SchemaParserOptions` you need to inject the custom missing resolver data fetcher:
```kotlin
SchemaParserOptions.newOptions()
  .missingResolverDataFetcher(ExampleMissingResolverDataFetcher())
  .build()
```

## Pending Work / Feedback Requested:
The following pending work remains and I am also requesting feedback here:
* Is this the right approach to allow catch-all field resolver?
* Some feedback on test coverage and if more unit tests should be added.
* Updating developer documentation.

Patch:
diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt
index 411236e0..0f290e5c 100644
--- a/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt
@@ -5,6 +5,7 @@ import graphql.kickstart.tools.proxy.*
 import graphql.kickstart.tools.relay.RelayConnectionFactory
 import graphql.kickstart.tools.util.JavaType
 import graphql.kickstart.tools.util.ParameterizedTypeImpl
+import graphql.schema.DataFetcher
 import graphql.schema.DataFetchingEnvironment
 import graphql.schema.visibility.GraphqlFieldVisibility
 import kotlinx.coroutines.Dispatchers
@@ -22,6 +23,7 @@ data class SchemaParserOptions internal constructor(
     val contextClass: Class<*>?,
     val genericWrappers: List<GenericWrapper>,
     val allowUnimplementedResolvers: Boolean,
+    val missingResolverDataFetcher: DataFetcher<Any?>?,
     val objectMapperProvider: PerFieldObjectMapperProvider,
     val proxyHandlers: List<ProxyHandler>,
     val inputArgumentOptionalDetectOmission: Boolean,
@@ -49,6 +51,7 @@ data class SchemaParserOptions internal constructor(
         private val genericWrappers: MutableList<GenericWrapper> = mutableListOf()
         private var useDefaultGenericWrappers = true
         private var allowUnimplementedResolvers = false
+        private var missingResolverDataFetcher: DataFetcher<Any?>? = null
         private var objectMapperProvider: PerFieldObjectMapperProvider = PerFieldConfiguringObjectMapperProvider()
         private val proxyHandlers: MutableList<ProxyHandler> = mutableListOf(Spring4AopProxyHandler(), GuiceAopProxyHandler(), JavassistProxyHandler(), WeldProxyHandler())
         private var inputArgumentOptionalDetectOmission = false
@@ -83,6 +86,10 @@ data class SchemaParserOptions internal constructor(
             this.allowUnimplementedResolvers = allowUnimplementedResolvers
         }
 
+        fun missingResolverDataFetcher(missingResolverDataFetcher: DataFetcher<Any?>?) = this.apply {
+            this.missingResolverDataFetcher = missingResolverDataFetcher
+        }
+
         fun inputArgumentOptionalDetectOmission(inputArgumentOptionalDetectOmission: Boolean) = this.apply {
             this.inputArgumentOptionalDetectOmission = inputArgumentOptionalDetectOmission
         }
@@ -161,6 +168,7 @@ data class SchemaParserOptions internal constructor(
                 contextClass,
                 wrappers,
                 allowUnimplementedResolvers,
+                missingResolverDataFetcher,
                 objectMapperProvider,
                 proxyHandlers,
                 inputArgumentOptionalDetectOmission,
diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt
index ea8ec966..f89d4138 100644
--- a/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt
@@ -61,8 +61,10 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
     }
 
     private fun missingFieldResolver(field: FieldDefinition, searches: List<Search>, scanProperties: Boolean): FieldResolver {
-        return if (options.allowUnimplementedResolvers) {
-            log.warn("Missing resolver for field: $field")
+        return if (options.allowUnimplementedResolvers || options.missingResolverDataFetcher != null) {
+            if (options.allowUnimplementedResolvers) {
+                log.warn("Missing resolver for field: $field")
+            }
 
             MissingFieldResolver(field, options)
         } else {
diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/MissingFieldResolver.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/MissingFieldResolver.kt
index 2a102493..bb925530 100644
--- a/src/main/kotlin/graphql/kickstart/tools/resolver/MissingFieldResolver.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/resolver/MissingFieldResolver.kt
@@ -12,6 +12,6 @@ internal class MissingFieldResolver(
 ) : FieldResolver(field, FieldResolverScanner.Search(Any::class.java, MissingResolverInfo(), null), options, Any::class.java) {
 
     override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> = listOf()
-
-    override fun createDataFetcher(): DataFetcher<*> = DataFetcher<Any> { TODO("Schema resolver not implemented") }
+    override fun createDataFetcher(): DataFetcher<*> =
+        options.missingResolverDataFetcher ?: DataFetcher<Any> { TODO("Schema resolver not implemented") }
 }


Test patch:
diff --git a/src/test/kotlin/graphql/kickstart/tools/MissingFieldResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/MissingFieldResolverTest.kt
new file mode 100644
index 00000000..c71173fe
--- /dev/null
+++ b/src/test/kotlin/graphql/kickstart/tools/MissingFieldResolverTest.kt
@@ -0,0 +1,73 @@
+package graphql.kickstart.tools
+
+import graphql.GraphQL
+import graphql.kickstart.tools.resolver.FieldResolverError
+import graphql.schema.DataFetcher
+import graphql.schema.DataFetchingEnvironment
+import org.junit.Test
+import java.util.*
+
+class MissingFieldResolverTest {
+
+    @Test(expected = FieldResolverError::class)
+    fun `should throw error when a field is missing`() {
+        SchemaParser.newParser()
+            .schemaString(
+                """
+                type Query {
+                    implementedField(input: String): String
+                    missingField(input: Int): Int
+                }
+                """
+            )
+            .resolvers(object : GraphQLQueryResolver {
+                fun implementedField(input: Optional<String>) = input.toString()
+            })
+            .build()
+            .makeExecutableSchema()
+    }
+
+    @Test
+    fun `should call missing resolver data fetcher if provided`() {
+        val schema = SchemaParser.newParser()
+            .schemaString(
+                """
+                type Query {
+                    implementedField(input: String): String
+                    missingField(input: Int): Int
+                }
+                """
+            )
+            .resolvers(object : GraphQLQueryResolver {
+                fun implementedField(input: Optional<String>) = input.toString()
+            })
+            .options(SchemaParserOptions.newOptions()
+                .missingResolverDataFetcher(TestMissingResolverDataFetcher())
+                .build())
+            .build()
+            .makeExecutableSchema()
+
+        val gql = GraphQL.newGraphQL(schema).build()
+
+        val result = gql.execute(
+            """
+            query {
+                implementedField(input: "test-value")
+                missingField(input: 1)
+            }
+            """)
+
+        val expected = mapOf(
+            "implementedField" to "Optional[test-value]",
+            "missingField" to 1
+        )
+
+        assertEquals(result.getData(), expected)
+    }
+
+    class TestMissingResolverDataFetcher : DataFetcher<Any?> {
+        override fun get(env: DataFetchingEnvironment?): Any? {
+            return env?.getArgument("input")
+        }
+    }
+}