@@ -7,7 +7,9 @@ import graphql.kickstart.tools.RootResolverInfo
77import graphql.kickstart.tools.SchemaParserOptions
88import graphql.kickstart.tools.util.*
99import graphql.language.FieldDefinition
10+ import graphql.language.InputValueDefinition
1011import graphql.schema.DataFetchingEnvironment
12+ import graphql.schema.idl.ScalarInfo
1113import kotlinx.coroutines.channels.ReceiveChannel
1214import org.apache.commons.lang3.ClassUtils
1315import org.apache.commons.lang3.reflect.FieldUtils
@@ -25,7 +27,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
2527
2628 private val log = LoggerFactory .getLogger(javaClass)
2729
28- val allowedTrailingParameterTypes = listOfNotNull(DataFetchingEnvironment ::class .java, GraphQLContext ::class .java, options.contextClass)
30+ private val allowedTrailingParameterTypes = listOfNotNull(DataFetchingEnvironment ::class .java, GraphQLContext ::class .java, options.contextClass)
2931
3032 private val methodsByNameCache = mutableMapOf<Pair <Class <out Any >, Boolean > , Map <String , List <Method >>>()
3133
@@ -39,7 +41,10 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
3941 throw FieldResolverError (" Found more than one matching resolver for field '$field ': $found " )
4042 }
4143
42- return found.firstOrNull() ? : missingFieldResolver(field, searches, scanProperties)
44+ return found.firstOrNull()
45+ ? : missingFieldResolver(field)
46+ ? : findSourcelessFieldResolver(field, searches)
47+ ? : throw FieldResolverError (getMissingFieldMessage(field, searches, scanProperties))
4348 }
4449
4550 private fun findFieldResolver (field : FieldDefinition , search : Search , scanProperties : Boolean ): FieldResolver ? {
@@ -71,7 +76,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
7176 }
7277 }
7378
74- private fun missingFieldResolver (field : FieldDefinition , searches : List < Search >, scanProperties : Boolean ): FieldResolver {
79+ private fun missingFieldResolver (field : FieldDefinition ): FieldResolver ? {
7580 @Suppress(" DEPRECATION" )
7681 return if (options.allowUnimplementedResolvers
7782 || options.missingResolverDataFetcher != null
@@ -82,13 +87,44 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
8287
8388 MissingFieldResolver (field, options)
8489 } else {
85- throw FieldResolverError (getMissingFieldMessage(field, searches, scanProperties))
90+ null
8691 }
8792 }
8893
94+ private fun findSourcelessFieldResolver (field : FieldDefinition , searches : List <Search >): FieldResolver ? =
95+ searches.firstNotNullOfOrNull { search ->
96+ val sourceType = search.requiredFirstParameterType ? : return @firstNotNullOfOrNull null
97+ val sourceless = search.withoutSourceParameter()
98+ findMethodByName(field, sourceless) { isSourcelessMethod(it, field, sourceless, sourceType) }?.let { method ->
99+ MethodFieldResolver (field, sourceless, options, method.apply (trySetAccessible(field, search.type)), scalarNames)
100+ }
101+ }
102+
103+ private fun isSourcelessMethod (method : Method , field : FieldDefinition , search : Search , sourceType : Class <* >): Boolean {
104+ val arguments = field.inputValueDefinitions
105+ val parameterTypes = method.parameterTypes
106+
107+ return method.parameterCountWithoutContinuation() > arguments.size &&
108+ arguments.indices.none { isContextParameter(parameterTypes[it], arguments[it]) } &&
109+ (arguments.isEmpty() || ! couldTakeSource(parameterTypes[0 ], sourceType)) &&
110+ verifyMethodArguments(method, arguments.size, search)
111+ }
112+
113+ private fun couldTakeSource (parameterType : Class <* >, sourceType : Class <* >) =
114+ sourceType.isAssignableFrom(parameterType) || parameterType.isAssignableFrom(sourceType)
115+
116+ // the context class can also be an argument's type (e.g. a Map for an input object), so only spec scalars count for it
117+ fun isContextParameter (parameter : Class <* >, argument : InputValueDefinition ): Boolean =
118+ parameter in allowedTrailingParameterTypes &&
119+ (parameter != options.contextClass || argument.type.unwrap().name in ScalarInfo .GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS )
120+
89121 private fun findResolverMethod (field : FieldDefinition , search : Search ): Method ? {
122+ val argumentCount = field.inputValueDefinitions.size + search.firstArgumentIndex
123+ return findMethodByName(field, search) { verifyMethodArguments(it, argumentCount, search) }
124+ }
125+
126+ private inline fun findMethodByName (field : FieldDefinition , search : Search , matches : (Method ) -> Boolean ): Method ? {
90127 val methodsByName = getMethodsByName(search)
91- val argumentCount = field.inputValueDefinitions.size + if (search.requiredFirstParameterType != null ) 1 else 0
92128 val name = field.name
93129 val capitalizedName = name.replaceFirstChar(Char ::titlecase)
94130
@@ -105,7 +141,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
105141 " getField$capitalizedName " ,
106142 " get${name.snakeToCamelCase()} "
107143 ).firstNotNullOfOrNull { methodName ->
108- methodsByName[methodName]?.find { verifyMethodArguments (it, argumentCount, search ) }
144+ methodsByName[methodName]?.find { matches (it) }
109145 }
110146 }
111147
@@ -197,6 +233,9 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
197233 signatures.addAll(getMissingMethodSignatures(field, search, isBoolean, scannedProperties))
198234 isSubscription = isSubscription || search.isSubscription
199235 }
236+ searches.filter { it.requiredFirstParameterType != null }.forEach { search ->
237+ signatures.addAll(getMissingMethodSignatures(field, search.withoutSourceParameter(), isBoolean, false , " <environment or context>" ))
238+ }
200239
201240 val sourceName = field.sourceLocation?.sourceName ? : " <unknown>"
202241 val sourceLocation = field.sourceLocation?.let { " $sourceName :${it.line} " } ? : " <unknown>"
@@ -206,7 +245,13 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
206245 if (isSubscription) " \n\n Note that a Subscription data fetcher must return a Publisher of events" else " "
207246 }
208247
209- private fun getMissingMethodSignatures (field : FieldDefinition , search : Search , isBoolean : Boolean , scannedProperties : Boolean ): List <String > {
248+ private fun getMissingMethodSignatures (
249+ field : FieldDefinition ,
250+ search : Search ,
251+ isBoolean : Boolean ,
252+ scannedProperties : Boolean ,
253+ trailingParameter : String? = null
254+ ): List <String > {
210255 val baseType = search.type.unwrap()
211256 val signatures = mutableListOf<String >()
212257 val args = mutableListOf<String >()
@@ -217,6 +262,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
217262 }
218263
219264 args.addAll(field.inputValueDefinitions.map { " ~${it.name} " })
265+ trailingParameter?.let (args::add)
220266
221267 val argString = args.joinToString(sep)
222268
@@ -239,6 +285,10 @@ internal class FieldResolverScanner(val options: SchemaParserOptions, private va
239285 val requiredFirstParameterType : Class <* >? = null
240286 ) {
241287 val isSubscription get() = resolverInfo is RootResolverInfo && resolverInfo.isSubscription
288+
289+ val firstArgumentIndex get() = if (requiredFirstParameterType != null ) 1 else 0
290+
291+ fun withoutSourceParameter () = copy(requiredFirstParameterType = null )
242292 }
243293}
244294
0 commit comments