Skip to content

Commit fc24426

Browse files
authored
Merge pull request #918 from graphql-java-kickstart/feature/905-replay-unwrap
Unwrap nested return values the way the schema build matched them
2 parents 5c647bf + 66d1bbd commit fc24426

5 files changed

Lines changed: 274 additions & 208 deletions

File tree

‎src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal class GenericTypeResolver(private val containingType: JavaType, private
2929
* - wildcards are replaced by their upper bound, e.g. `? extends Foo` becomes `Foo`
3030
* - primitives are replaced by their boxed class, e.g. `int` becomes `Integer`
3131
*/
32-
fun resolve(javaType: JavaType, onUnwrap: ((wrapped: ParameterizedType) -> Unit)? = null): JavaType {
32+
fun resolve(javaType: JavaType, onUnwrap: ((SchemaParserOptions.GenericWrapper) -> Unit)? = null): JavaType {
3333
return when (val type = resolveTypeVariables(javaType)) {
3434
is ParameterizedType -> {
3535
val rawType = type.rawType
@@ -41,7 +41,7 @@ internal class GenericTypeResolver(private val containingType: JavaType, private
4141
throw IndexOutOfBoundsException("Generic type '${TypeUtils.toString(type)}' does not have a type argument at index ${wrapper.index}!")
4242
}
4343

44-
onUnwrap?.invoke(type)
44+
onUnwrap?.invoke(wrapper)
4545
val unwrapsTo = wrapper.schemaWrapper.invoke(typeArguments[wrapper.index])
4646
resolve(unwrapsTo, onUnwrap)
4747
}
Lines changed: 82 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package graphql.kickstart.tools
22

33
import graphql.execution.DataFetcherResult
4+
import graphql.kickstart.tools.SchemaParserOptions.GenericWrapper
45
import graphql.kickstart.tools.util.GraphQLLangType
56
import graphql.kickstart.tools.util.JavaType
67
import graphql.kickstart.tools.util.isSubtypeOf
@@ -15,79 +16,22 @@ import java.util.*
1516
*/
1617
internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeDefinition<*>>) {
1718

18-
private fun error(potentialMatch: PotentialMatch, msg: String) = SchemaClassScannerError("Unable to match type definition (${TypeUtil.simplePrint(potentialMatch.graphQLType)}) for reference ${potentialMatch.reference} with java type (${potentialMatch.javaType}): $msg")
19-
2019
fun match(potentialMatch: PotentialMatch): Match {
21-
return match(potentialMatch, potentialMatch.graphQLType, potentialMatch.javaType, true)
22-
}
23-
24-
private fun match(potentialMatch: PotentialMatch, graphQLType: GraphQLLangType, javaType: JavaType, root: Boolean = false): Match {
25-
26-
var realType = potentialMatch.typeResolver.resolve(javaType)
27-
28-
if (realType is ParameterizedType && realType.isSubtypeOf(DataFetcherResult::class.java)) {
29-
if (potentialMatch.location != Location.RETURN_TYPE) {
30-
throw error(potentialMatch, "${DataFetcherResult::class.java.name} can only be used as a return type")
31-
}
32-
33-
realType = potentialMatch.typeResolver.resolve(realType.actualTypeArguments.first())
34-
35-
if (realType is ParameterizedType && realType.isSubtypeOf(DataFetcherResult::class.java)) {
36-
throw error(potentialMatch, "${DataFetcherResult::class.java.name} cannot be nested within itself")
37-
}
38-
}
39-
40-
var optional = false
41-
42-
// Handle jdk8 Optionals
43-
if (realType is ParameterizedType && realType.isSubtypeOf(Optional::class.java)) {
44-
optional = true
45-
46-
if (potentialMatch.location == Location.RETURN_TYPE && !root) {
47-
throw error(potentialMatch, "${Optional::class.java.name} can only be used at the top level of a return type")
48-
}
49-
50-
realType = potentialMatch.typeResolver.resolve(realType.actualTypeArguments.first())
51-
52-
if (realType is ParameterizedType && realType.isSubtypeOf(Optional::class.java)) {
53-
throw error(potentialMatch, "${Optional::class.java.name} cannot be nested within itself")
54-
}
55-
}
56-
57-
// Match graphql type to java type.
58-
return when (graphQLType) {
59-
is NonNullType -> {
60-
if (optional) {
61-
throw error(potentialMatch, "graphql type is marked as nonnull but ${Optional::class.java.name} was used")
62-
}
63-
match(potentialMatch, graphQLType.type, realType)
64-
}
65-
66-
is ListType -> when {
67-
realType is ParameterizedType && realType.isSubtypeOf(Iterable::class.java) ->
68-
match(potentialMatch, graphQLType.type, realType.actualTypeArguments.first())
69-
realType is Class<*> && realType.isArray ->
70-
match(potentialMatch, graphQLType.type, realType.componentType)
71-
// e.g. a property map value: the element type is unknown, so match it as Object too
72-
realType == Any::class.java ->
73-
match(potentialMatch, graphQLType.type, realType)
74-
else ->
75-
throw error(potentialMatch, "Java class is not a List or generic type information was lost: $realType")
76-
}
77-
20+
val unwrapping = potentialMatch.unwrapping
21+
return when (val graphQLType = unwrapping.graphQLType) {
7822
is TypeName -> {
7923
val typeDefinition = ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS[graphQLType.name]
8024
?: definitionsByName[graphQLType.name]
81-
?: throw error(potentialMatch, "No ${TypeDefinition::class.java.simpleName} for type name ${graphQLType.name}")
25+
?: throw potentialMatch.error("No ${TypeDefinition::class.java.simpleName} for type name ${graphQLType.name}")
8226
if (typeDefinition is ScalarTypeDefinition) {
8327
ScalarMatch(typeDefinition)
8428
} else {
85-
ValidMatch(typeDefinition, realType, potentialMatch.reference)
29+
ValidMatch(typeDefinition, unwrapping.javaType, potentialMatch.reference)
8630
}
8731
}
8832

89-
is TypeDefinition<*> -> ValidMatch(graphQLType, realType, potentialMatch.reference)
90-
else -> throw error(potentialMatch, "Unknown type: ${realType.javaClass.name}")
33+
is TypeDefinition<*> -> ValidMatch(graphQLType, unwrapping.javaType, potentialMatch.reference)
34+
else -> throw potentialMatch.error("Unknown type: ${unwrapping.javaType.javaClass.name}")
9135
}
9236
}
9337

@@ -109,6 +53,72 @@ internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeD
10953
val reference: ScanDictionary.Reference,
11054
val location: Location
11155
) {
56+
val unwrapping: Unwrapping by lazy { unwrap(graphQLType, javaType, root = true, mutableListOf()) }
57+
58+
fun error(msg: String) = SchemaClassScannerError("Unable to match type definition (${TypeUtil.simplePrint(graphQLType)}) for reference $reference with java type ($javaType): $msg")
59+
60+
private fun unwrap(graphQLType: GraphQLLangType, javaType: JavaType, root: Boolean, unwrapped: MutableList<Unwrapped>): Unwrapping {
61+
val onUnwrap = { wrapper: GenericWrapper -> unwrapped += Unwrapped.Wrapper(wrapper) }
62+
var realType = typeResolver.resolve(javaType, onUnwrap)
63+
64+
if (realType is ParameterizedType && realType.isSubtypeOf(DataFetcherResult::class.java)) {
65+
if (location != Location.RETURN_TYPE) {
66+
throw error("${DataFetcherResult::class.java.name} can only be used as a return type")
67+
}
68+
69+
unwrapped += Unwrapped.Data
70+
realType = typeResolver.resolve(realType.actualTypeArguments.first(), onUnwrap)
71+
72+
if (realType is ParameterizedType && realType.isSubtypeOf(DataFetcherResult::class.java)) {
73+
throw error("${DataFetcherResult::class.java.name} cannot be nested within itself")
74+
}
75+
}
76+
77+
var optional = false
78+
79+
// Handle jdk8 Optionals
80+
if (realType is ParameterizedType && realType.isSubtypeOf(Optional::class.java)) {
81+
optional = true
82+
83+
if (location == Location.RETURN_TYPE && !root) {
84+
throw error("${Optional::class.java.name} can only be used at the top level of a return type")
85+
}
86+
87+
unwrapped += Unwrapped.Present
88+
realType = typeResolver.resolve(realType.actualTypeArguments.first(), onUnwrap)
89+
90+
if (realType is ParameterizedType && realType.isSubtypeOf(Optional::class.java)) {
91+
throw error("${Optional::class.java.name} cannot be nested within itself")
92+
}
93+
}
94+
95+
// Match graphql type to java type.
96+
return when (graphQLType) {
97+
is NonNullType -> {
98+
if (optional) {
99+
throw error("graphql type is marked as nonnull but ${Optional::class.java.name} was used")
100+
}
101+
unwrap(graphQLType.type, realType, root = false, unwrapped)
102+
}
103+
104+
is ListType -> when {
105+
realType is ParameterizedType && realType.isSubtypeOf(Iterable::class.java) -> {
106+
unwrapped += Unwrapped.Elements
107+
unwrap(graphQLType.type, realType.actualTypeArguments.first(), root = false, unwrapped)
108+
}
109+
realType is Class<*> && realType.isArray ->
110+
unwrap(graphQLType.type, realType.componentType, root = false, unwrapped)
111+
// e.g. a property map value: the element type is unknown, so match it as Object too
112+
realType == Any::class.java ->
113+
unwrap(graphQLType.type, realType, root = false, unwrapped)
114+
else ->
115+
throw error("Java class is not a List or generic type information was lost: $realType")
116+
}
117+
118+
else -> Unwrapping(unwrapped, graphQLType, realType)
119+
}
120+
}
121+
112122
companion object {
113123
fun returnValue(graphQLType: GraphQLLangType, javaType: JavaType, typeResolver: GenericTypeResolver, reference: ScanDictionary.Reference) =
114124
PotentialMatch(graphQLType, javaType, typeResolver, reference, Location.RETURN_TYPE)
@@ -117,4 +127,13 @@ internal class TypeClassMatcher(private val definitionsByName: Map<String, TypeD
117127
PotentialMatch(graphQLType, javaType, typeResolver, reference, Location.PARAMETER_TYPE)
118128
}
119129
}
130+
131+
internal class Unwrapping(val unwrapped: List<Unwrapped>, val graphQLType: GraphQLLangType, val javaType: JavaType)
132+
133+
internal sealed interface Unwrapped {
134+
class Wrapper(val wrapper: GenericWrapper) : Unwrapped
135+
object Data : Unwrapped
136+
object Present : Unwrapped
137+
object Elements : Unwrapped
138+
}
120139
}

‎src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,7 @@ internal class MethodFieldResolver(
130130
// Object (e.g. for a union) can't tell them apart, so then every future of a method whose return type is Future is checked
131131
val valueClass = (TypeUtils.getRawType(getResolvedReturnType(), null) ?: Any::class.java)
132132
.takeUnless { it == Any::class.java && TypeUtils.getRawType(returnType, null) == Future::class.java }
133-
val unwrapper = ReturnValueUnwrapper.create(
134-
method, returnType, valueClass, field.type, search.isSubscription, typeResolver, options.genericWrappers, scalarNames
135-
)
133+
val unwrapper = ReturnValueUnwrapper.create(method, returnValueMatch.unwrapping, valueClass, isSuspendFunction, search.isSubscription, options.genericWrappers)
136134

137135
return if (numberOfParameters > 0 || isSuspendFunction) {
138136
// requires arguments and environment or is a suspend function
@@ -160,8 +158,6 @@ internal class MethodFieldResolver(
160158
}
161159

162160
override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> {
163-
val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, getResolvedReturnType(), typeResolver, ScanDictionary.ReturnValueReference(method))
164-
165161
return field.inputValueDefinitions.mapIndexed { i, inputDefinition ->
166162
TypeClassMatcher.PotentialMatch.parameterType(inputDefinition.type, getMethodParameterType(i)!!, typeResolver, ScanDictionary.MethodParameterReference(method, i))
167163
} + listOf(returnValueMatch)
@@ -184,8 +180,13 @@ internal class MethodFieldResolver(
184180
typeResolver.resolveTypeVariables(declaredType)
185181
}
186182

187-
private fun getResolvedReturnType(): JavaType =
188-
typeResolver.resolve(if (search.isSubscription) returnType.asPublisher() else returnType)
183+
private val returnValueMatch by lazy {
184+
TypeClassMatcher.PotentialMatch.returnValue(field.type, matchedReturnType(), typeResolver, ScanDictionary.ReturnValueReference(method))
185+
}
186+
187+
private fun matchedReturnType(): JavaType = if (search.isSubscription) returnType.asPublisher() else returnType
188+
189+
private fun getResolvedReturnType(): JavaType = typeResolver.resolve(matchedReturnType())
189190

190191
private fun getJavaMethodParameterIndex(index: Int) = index + search.firstArgumentIndex
191192

0 commit comments

Comments
 (0)