Skip to content

Commit a33a663

Browse files
committed
Make relay connection test succeed using generic types
1 parent 0d857e3 commit a33a663

File tree

13 files changed

+177
-91
lines changed

13 files changed

+177
-91
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.coxautodev.graphql.tools
2+
3+
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
4+
import java.util.*
5+
6+
internal data class ClassEntry(val type: JavaType, val clazz: Class<out Any>, val typedArguments: Array<out Any> = arrayOf()) {
7+
companion object {
8+
fun of(clazz: Class<out Any>) = ClassEntry(clazz, clazz)
9+
10+
fun of(javaType: JavaType) =
11+
if (javaType is ParameterizedTypeImpl) {
12+
val list = arrayOf(javaType.actualTypeArguments.map { type -> type.javaClass })
13+
ClassEntry(javaType, javaType.rawType, list)
14+
} else {
15+
ClassEntry(javaType, javaType as Class<out Any>)
16+
}
17+
}
18+
19+
override fun equals(other: Any?): Boolean {
20+
if (this === other) return true
21+
if (other !is ClassEntry) return false
22+
23+
if (clazz != other.clazz) return false
24+
if (!Arrays.equals(typedArguments, other.typedArguments)) return false
25+
26+
return true
27+
}
28+
29+
override fun hashCode(): Int {
30+
var result = clazz.hashCode()
31+
result = 31 * result + Arrays.hashCode(typedArguments)
32+
return result
33+
}
34+
}
35+

src/main/kotlin/com/coxautodev/graphql/tools/DictionaryTypeResolver.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import graphql.schema.TypeResolver
1111
/**
1212
* @author Andrew Potter
1313
*/
14-
abstract class DictionaryTypeResolver(private val dictionary: BiMap<Class<*>, TypeDefinition<*>>, private val types: Map<String, GraphQLObjectType>) : TypeResolver {
14+
abstract class DictionaryTypeResolver(private val dictionary: BiMap<JavaType, TypeDefinition<*>>, private val types: Map<String, GraphQLObjectType>) : TypeResolver {
1515
private fun <T> getTypeName(clazz: Class<T>): String? {
1616
val name = dictionary[clazz]?.name
1717

@@ -34,11 +34,11 @@ abstract class DictionaryTypeResolver(private val dictionary: BiMap<Class<*>, Ty
3434
abstract fun getError(name: String): String
3535
}
3636

37-
class InterfaceTypeResolver(dictionary: BiMap<Class<*>, TypeDefinition<*>>, private val thisInterface: GraphQLInterfaceType, types: List<GraphQLObjectType>) : DictionaryTypeResolver(dictionary, types.filter { it.interfaces.any { it.name == thisInterface.name } }.associateBy { it.name }) {
37+
class InterfaceTypeResolver(dictionary: BiMap<JavaType, TypeDefinition<*>>, private val thisInterface: GraphQLInterfaceType, types: List<GraphQLObjectType>) : DictionaryTypeResolver(dictionary, types.filter { it.interfaces.any { it.name == thisInterface.name } }.associateBy { it.name }) {
3838
override fun getError(name: String) = "Expected object type with name '$name' to implement interface '${thisInterface.name}', but it doesn't!"
3939
}
4040

41-
class UnionTypeResolver(dictionary: BiMap<Class<*>, TypeDefinition<*>>, private val thisUnion: GraphQLUnionType, types: List<GraphQLObjectType>) : DictionaryTypeResolver(dictionary, types.filter { type -> thisUnion.types.any { it.name == type.name } }.associateBy { it.name }) {
41+
class UnionTypeResolver(dictionary: BiMap<JavaType, TypeDefinition<*>>, private val thisUnion: GraphQLUnionType, types: List<GraphQLObjectType>) : DictionaryTypeResolver(dictionary, types.filter { type -> thisUnion.types.any { it.name == type.name } }.associateBy { it.name }) {
4242
override fun getError(name: String) = "Expected object type with name '$name' to exist for union '${thisUnion.name}', but it doesn't!"
4343
}
4444

src/main/kotlin/com/coxautodev/graphql/tools/FieldResolverScanner.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
2020
companion object {
2121
private val log = LoggerFactory.getLogger(FieldResolverScanner::class.java)
2222

23-
fun getAllMethods(type: Class<*>) =
24-
(type.declaredMethods.toList() + ClassUtils.getAllSuperclasses(type).flatMap { it.methods.toList() })
23+
fun getAllMethods(type: JavaType) =
24+
(type.unwrap().declaredMethods.toList() + ClassUtils.getAllSuperclasses(type.unwrap()).flatMap { it.methods.toList() })
2525
.asSequence()
2626
.filter { !it.isSynthetic }
2727
.filter { !Modifier.isPrivate(it.modifiers) }
@@ -57,6 +57,10 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
5757
private fun findFieldResolver(field: FieldDefinition, search: Search, scanProperties: Boolean): FieldResolver? {
5858
val method = findResolverMethod(field, search)
5959
if (method != null) {
60+
if (search.type is ParameterizedType && search.type.actualTypeArguments.isNotEmpty()) {
61+
log.debug("actual type parameters: ${search.type.actualTypeArguments[0]}")
62+
}
63+
log.debug("method hit: ${method} and search type ${search.type} and declaring class ${method.declaringClass}")
6064
return MethodFieldResolver(field, search, options, method.apply { isAccessible = true })
6165
}
6266

@@ -131,7 +135,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
131135
}
132136

133137
private fun findResolverProperty(field: FieldDefinition, search: Search) =
134-
FieldUtils.getAllFields(search.type).find { it.name == field.name }
138+
FieldUtils.getAllFields(search.type.unwrap()).find { it.name == field.name }
135139

136140
private fun getMissingFieldMessage(field: FieldDefinition, searches: List<Search>, scannedProperties: Boolean): String {
137141
val signatures = mutableListOf("")
@@ -147,7 +151,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
147151
}
148152

149153
private fun getMissingMethodSignatures(field: FieldDefinition, search: Search, isBoolean: Boolean, scannedProperties: Boolean): List<String> {
150-
val baseType = search.type
154+
val baseType = search.type.unwrap()
151155
val signatures = mutableListOf<String>()
152156
val args = mutableListOf<String>()
153157
val sep = ", "
@@ -172,7 +176,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
172176
return signatures
173177
}
174178

175-
data class Search(val type: Class<*>, val resolverInfo: ResolverInfo, val source: Any?, val requiredFirstParameterType: Class<*>? = null, val allowBatched: Boolean = false)
179+
data class Search(val type: JavaType, val resolverInfo: ResolverInfo, val source: Any?, val requiredFirstParameterType: Class<*>? = null, val allowBatched: Boolean = false)
176180
}
177181

178182
class FieldResolverError(msg: String) : RuntimeException(msg)

src/main/kotlin/com/coxautodev/graphql/tools/GenericType.kt

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.coxautodev.graphql.tools
22

33
import com.google.common.primitives.Primitives
44
import org.apache.commons.lang3.reflect.TypeUtils
5+
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
56
import sun.reflect.generics.reflectiveObjects.WildcardTypeImpl
67
import java.lang.reflect.ParameterizedType
78
import java.lang.reflect.TypeVariable
@@ -78,7 +79,8 @@ internal open class GenericType(protected val mostSpecificType: JavaType, protec
7879
/**
7980
* Unwrap certain Java types to find the "real" class.
8081
*/
81-
fun unwrapGenericType(type: JavaType): JavaType {
82+
fun unwrapGenericType(javaType: JavaType): JavaType {
83+
val type = replaceTypeVariable(javaType)
8284
return when (type) {
8385
is ParameterizedType -> {
8486
val rawType = type.rawType
@@ -89,27 +91,41 @@ internal open class GenericType(protected val mostSpecificType: JavaType, protec
8991
throw IndexOutOfBoundsException("Generic type '${TypeUtils.toString(type)}' does not have a type argument at index ${genericType.index}!")
9092
}
9193

92-
val unwrapsTo = if (genericType.schemaWrapper != null) {
93-
genericType.schemaWrapper.invoke(typeArguments[genericType.index])
94-
} else {
95-
typeArguments[genericType.index]
96-
}
97-
94+
val unwrapsTo = genericType.schemaWrapper.invoke(typeArguments[genericType.index])
9895
return unwrapGenericType(unwrapsTo)
9996
}
10097
is Class<*> -> if (type.isPrimitive) Primitives.wrap(type) else type
10198
is TypeVariable<*> -> {
10299
if (declaringType !is ParameterizedType) {
103100
error("Could not resolve type variable '${TypeUtils.toLongString(type)}' because declaring type is not parameterized: ${TypeUtils.toString(declaringType)}")
101+
} else {
102+
unwrapGenericType(TypeUtils.determineTypeArguments(getRawClass(mostSpecificType), declaringType)[type]
103+
?: error("No type variable found for: ${TypeUtils.toLongString(type)}"))
104104
}
105-
106-
unwrapGenericType(TypeUtils.determineTypeArguments(getRawClass(mostSpecificType), declaringType)[type]
107-
?: error("No type variable found for: ${TypeUtils.toLongString(type)}"))
108105
}
109106
is WildcardTypeImpl -> type.upperBounds.firstOrNull()
110107
?: throw error("Unable to unwrap type, wildcard has no upper bound: $type")
111108
else -> error("Unable to unwrap type: $type")
112109
}
113110
}
111+
112+
private fun replaceTypeVariable(type: JavaType): JavaType {
113+
return when(type) {
114+
is ParameterizedType -> {
115+
val actualTypeArguments = type.actualTypeArguments.map { replaceTypeVariable(it) }.toTypedArray()
116+
ParameterizedTypeImpl.make(type.rawType as Class<*>?, actualTypeArguments, type.ownerType)
117+
}
118+
is TypeVariable<*> -> {
119+
if (declaringType is ParameterizedType) {
120+
TypeUtils.getRawType(type, declaringType)
121+
} else {
122+
type
123+
}
124+
}
125+
else -> {
126+
type
127+
}
128+
}
129+
}
114130
}
115131
}

src/main/kotlin/com/coxautodev/graphql/tools/MethodFieldResolver.kt

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,24 @@ import graphql.language.NonNullType
99
import graphql.schema.DataFetcher
1010
import graphql.schema.DataFetchingEnvironment
1111
import java.lang.reflect.Method
12+
import java.lang.reflect.ParameterizedType
13+
import java.lang.reflect.TypeVariable
1214
import java.util.*
1315

1416
/**
1517
* @author Andrew Potter
1618
*/
17-
internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolverScanner.Search, options: SchemaParserOptions, val method: Method) : FieldResolver(field, search, options, method.declaringClass) {
19+
internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolverScanner.Search, options: SchemaParserOptions, val method: Method) : FieldResolver(field, search, options, search.type) {
1820

1921
companion object {
2022
fun isBatched(method: Method, search: FieldResolverScanner.Search): Boolean {
2123
if (method.getAnnotation(Batched::class.java) != null) {
2224
if (!search.allowBatched) {
23-
throw ResolverError("The @Batched annotation is only allowed on non-root resolver methods, but it was found on ${search.type.name}#${method.name}!")
25+
throw ResolverError("The @Batched annotation is only allowed on non-root resolver methods, but it was found on ${search.type.unwrap().name}#${method.name}!")
2426
}
2527

2628
return true
2729
}
28-
2930
return false
3031
}
3132
}
@@ -98,13 +99,24 @@ internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolver
9899

99100
override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> {
100101
val batched = isBatched(method, search)
101-
val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, method.genericReturnType, genericType, SchemaClassScanner.ReturnValueReference(method), batched)
102+
// fixme: convert type variables
103+
// val replacedGenericType = replaceTypeVariables(method.genericReturnType)
104+
val unwrappedGenericType = genericType.unwrapGenericType(method.genericReturnType)
105+
val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, unwrappedGenericType, genericType, SchemaClassScanner.ReturnValueReference(method), batched)
102106

103107
return field.inputValueDefinitions.mapIndexed { i, inputDefinition ->
104108
TypeClassMatcher.PotentialMatch.parameterType(inputDefinition.type, getJavaMethodParameterType(i)!!, genericType, SchemaClassScanner.MethodParameterReference(method, i), batched)
105109
} + listOf(returnValueMatch)
106110
}
107111

112+
// private fun replaceTypeVariables(javaType: JavaType): JavaType {
113+
// return when (javaType) {
114+
// is ParameterizedType -> replaceTypeVariables(javaType.rawType)
115+
// is TypeVariable<*> -> genericType.unwrapGenericType(javaType)
116+
// else -> javaType
117+
// }
118+
// }
119+
108120
private fun getIndexOffset(): Int {
109121
return if (resolverInfo is DataClassTypeResolverInfo && !method.declaringClass.isAssignableFrom(resolverInfo.dataClassType)) {
110122
1

src/main/kotlin/com/coxautodev/graphql/tools/ResolverInfo.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ internal abstract class ResolverInfo {
1111
}
1212

1313
internal interface DataClassTypeResolverInfo {
14-
val dataClassType: Class<*>
14+
val dataClassType: Class<out Any>
1515
}
1616

1717
internal class NormalResolverInfo(val resolver: GraphQLResolver<*>, private val options: SchemaParserOptions) : DataClassTypeResolverInfo, ResolverInfo() {
1818
val resolverType = getRealResolverClass(resolver, options)
1919
override val dataClassType = findDataClass()
2020

21-
private fun findDataClass(): Class<*> {
21+
private fun findDataClass(): Class<out Any> {
2222
// Grab the parent interface with type GraphQLResolver from our resolver and get its first type argument.
2323
val interfaceType = GenericType(resolverType, options).getGenericInterface(GraphQLResolver::class.java)
2424
if (interfaceType == null || interfaceType !is ParameterizedType) {
@@ -52,8 +52,8 @@ internal class MultiResolverInfo(val resolverInfoList: List<NormalResolverInfo>)
5252
/**
5353
* Checks if all `ResolverInfo` instances are related to the same data type
5454
*/
55-
private fun findDataClass(): Class<*> {
56-
val dataClass = resolverInfoList.map { it.dataClassType }.distinct().singleOrNull()
55+
private fun findDataClass(): Class<out Any> {
56+
val dataClass = resolverInfoList.asSequence().map { it.dataClassType }.distinct().singleOrNull()
5757

5858
if (dataClass == null) {
5959
throw ResolverError("Resolvers may not use the same type.")
@@ -76,7 +76,7 @@ internal class RootResolverInfo(val resolvers: List<GraphQLRootResolver>, privat
7676
resolvers.map { FieldResolverScanner.Search(getRealResolverClass(it, options), this, it) }
7777
}
7878

79-
internal class DataClassResolverInfo(private val dataClass: Class<*>) : ResolverInfo() {
79+
internal class DataClassResolverInfo(private val dataClass: JavaType) : ResolverInfo() {
8080
override fun getFieldSearches() =
8181
listOf(FieldResolverScanner.Search(dataClass, this, null))
8282
}

src/main/kotlin/com/coxautodev/graphql/tools/ScannedSchemaObjects.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ internal data class ScannedSchemaObjects(
1818
val unusedDefinitions: Set<TypeDefinition<*>>
1919
)
2020

21-
internal typealias TypeClassDictionary = BiMap<TypeDefinition<*>, Class<out Any>>
21+
internal typealias TypeClassDictionary = BiMap<TypeDefinition<*>, JavaType>
2222
internal typealias CustomScalarMap = Map<String, GraphQLScalarType>

0 commit comments

Comments
 (0)