diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt index 8939bd00..589ebc48 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt @@ -29,6 +29,8 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) { private val allowedLastArgumentTypes = listOfNotNull(DataFetchingEnvironment::class.java, GraphQLContext::class.java, options.contextClass) + private val methodsByNameCache = mutableMapOf, Boolean>, Map>>() + fun findFieldResolver(field: FieldDefinition, resolverInfo: ResolverInfo): FieldResolver { val searches = resolverInfo.getFieldSearches() @@ -86,9 +88,10 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) { } private fun findResolverMethod(field: FieldDefinition, search: Search): Method? { - val methods = getAllMethods(search) + val methodsByName = getMethodsByName(search) val argumentCount = field.inputValueDefinitions.size + if (search.requiredFirstParameterType != null) 1 else 0 val name = field.name + val capitalizedName = name.replaceFirstChar(Char::titlecase) // Check for the following one by one: // 1. Method with exact field name @@ -96,19 +99,23 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) { // 3. Method with "get" style getter // 4. Method with "getField" style getter // 5. Method with "get" style getter with the field name converted from snake_case to camelCased. ex: key_ops -> getKeyOps() - return methods.find { - it.name == name && verifyMethodArguments(it, argumentCount, search) - } ?: methods.find { - (isBoolean(field.type) && it.name == "is${name.replaceFirstChar(Char::titlecase)}") && verifyMethodArguments(it, argumentCount, search) - } ?: methods.find { - it.name == "get${name.replaceFirstChar(Char::titlecase)}" && verifyMethodArguments(it, argumentCount, search) - } ?: methods.find { - it.name == "getField${name.replaceFirstChar(Char::titlecase)}" && verifyMethodArguments(it, argumentCount, search) - } ?: methods.find { - it.name == "get${name.snakeToCamelCase()}" && verifyMethodArguments(it, argumentCount, search) + return listOfNotNull( + name, + if (isBoolean(field.type)) "is$capitalizedName" else null, + "get$capitalizedName", + "getField$capitalizedName", + "get${name.snakeToCamelCase()}" + ).firstNotNullOfOrNull { methodName -> + methodsByName[methodName]?.find { verifyMethodArguments(it, argumentCount, search) } } } + // root fields are searched on every root resolver, so each class's methods are only indexed once + private fun getMethodsByName(search: Search): Map> = + methodsByNameCache.getOrPut(search.type.unwrap() to search.isSubscription) { + getAllMethods(search).groupBy { it.name } + } + private fun getAllMethods(search: Search): List { val type = search.type.unwrap() val declaredMethods = type.declaredNonProxyMethods diff --git a/src/test/kotlin/graphql/kickstart/tools/FieldResolverScannerTest.kt b/src/test/kotlin/graphql/kickstart/tools/FieldResolverScannerTest.kt index e5631844..4ca5fa54 100644 --- a/src/test/kotlin/graphql/kickstart/tools/FieldResolverScannerTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/FieldResolverScannerTest.kt @@ -98,6 +98,19 @@ class FieldResolverScannerTest { Locale.setDefault(default) } + @Test + fun `scanner finds field resolver methods in priority order`() { + val resolverInfo = RootResolverInfo(listOf(PriorityQuery()), options) + + fun methodName(name: String, type: String) = + (scanner.findFieldResolver(FieldDefinition(name, TypeName(type)), resolverInfo) as MethodFieldResolver).method.name + + assertEquals(methodName("name", "String"), "name") + assertEquals(methodName("active", "Boolean"), "isActive") + assertEquals(methodName("enabled", "String"), "getEnabled") + assertEquals(methodName("count", "Int"), "getFieldCount") + } + class RootQuery1 : GraphQLQueryResolver { fun field1() {} } @@ -118,6 +131,18 @@ class FieldResolverScannerTest { fun getId(): HullType = HullType() } + class PriorityQuery : GraphQLQueryResolver { + fun name(): String = "name" + fun getName(): String = "name" + fun getFieldName(): String = "name" + fun active(flag: Boolean): Boolean = flag + fun isActive(): Boolean = true + fun getActive(): Boolean = true + fun isEnabled(): Boolean = true + fun getEnabled(): String = "enabled" + fun getFieldCount(): Int = 1 + } + class HullType open class ParentPropertyQuery {