Skip to content

Commit 15c2972

Browse files
oryan-blockclaude
andcommitted
Merge branch 'master' into bugfix/479
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 parents 8a80a72 + 603d40e commit 15c2972

22 files changed

Lines changed: 737 additions & 228 deletions

‎.github/workflows/release.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ jobs:
3535
server-password: MAVEN_PASSWORD
3636
gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }}
3737
gpg-passphrase: GPG_PASSPHRASE
38+
- name: Setup Maven 3.9
39+
# Under Maven 3.10, central-publishing-maven-plugin 0.11.0 adds maven-metadata-local.xml to the bundle, which Central rejects
40+
run: |
41+
curl -sSfL https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.tar.gz | tar xz -C "$RUNNER_TEMP"
42+
echo "$RUNNER_TEMP/apache-maven-3.9.16/bin" >> "$GITHUB_PATH"
3843
- name: Publish release
3944
run: bash github-build.sh
4045
env:

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ kotlin.version=2.3.10
2626

2727
Add the dependency:
2828
```groovy
29-
compile 'io.github.graphql-java-kickstart:graphql-java-tools:14.1.0'
29+
compile 'io.github.graphql-java-kickstart:graphql-java-tools:14.2.0'
3030
```
3131

3232
### Using Maven
@@ -43,7 +43,7 @@ Add the dependency:
4343
<dependency>
4444
<groupId>io.github.graphql-java-kickstart</groupId>
4545
<artifactId>graphql-java-tools</artifactId>
46-
<version>14.1.0</version>
46+
<version>14.2.0</version>
4747
</dependency>
4848
```
4949

‎pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<groupId>io.github.graphql-java-kickstart</groupId>
66
<artifactId>graphql-java-tools</artifactId>
7-
<version>14.1.1-SNAPSHOT</version>
7+
<version>14.2.1-SNAPSHOT</version>
88
<packaging>jar</packaging>
99

1010
<name>GraphQL Java Tools</name>

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

Lines changed: 0 additions & 147 deletions
This file was deleted.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package graphql.kickstart.tools
2+
3+
import com.fasterxml.classmate.ResolvedType
4+
import graphql.kickstart.tools.util.JavaType
5+
import graphql.kickstart.tools.util.ParameterizedTypeImpl
6+
import graphql.kickstart.tools.util.Primitives
7+
import org.apache.commons.lang3.reflect.TypeUtils
8+
import java.lang.reflect.ParameterizedType
9+
import java.lang.reflect.TypeVariable
10+
import java.lang.reflect.WildcardType
11+
12+
/**
13+
* @author Andrew Potter
14+
*/
15+
internal class GenericTypeResolver(private val containingType: JavaType, private val options: SchemaParserOptions) {
16+
17+
fun getRawClass() = getRawClass(containingType)
18+
19+
fun getRawClass(type: JavaType): Class<*> = TypeUtils.getRawType(type, containingType)
20+
21+
fun isAssignableFrom(type: JavaType) = TypeUtils.isAssignable(type, containingType)
22+
23+
/**
24+
* Turns a type as declared in the source (a method's return or parameter type, or a field's type) into the type
25+
* the GraphQL type is matched against:
26+
* - type variables are resolved relative to the containing type,
27+
* e.g. `T` of `AbstractItem<T>.getId()` becomes `Long` for `class Item : AbstractItem<Long>()`
28+
* - generic wrappers are replaced by their wrapped type, e.g. `CompletableFuture<Foo>` becomes `Foo`
29+
* - wildcards are replaced by their upper bound, e.g. `? extends Foo` becomes `Foo`
30+
* - primitives are replaced by their boxed class, e.g. `int` becomes `Integer`
31+
*/
32+
fun resolve(javaType: JavaType): JavaType {
33+
return when (val type = resolveTypeVariables(javaType)) {
34+
is ParameterizedType -> {
35+
val rawType = type.rawType
36+
val wrapper = options.genericWrappers.find { it.type == rawType }
37+
?: return type
38+
39+
val typeArguments = type.actualTypeArguments
40+
if (typeArguments.size <= wrapper.index) {
41+
throw IndexOutOfBoundsException("Generic type '${TypeUtils.toString(type)}' does not have a type argument at index ${wrapper.index}!")
42+
}
43+
44+
val unwrapsTo = wrapper.schemaWrapper.invoke(typeArguments[wrapper.index])
45+
resolve(unwrapsTo)
46+
}
47+
is TypeVariable<*> -> error("Could not resolve type variable '${TypeUtils.toLongString(type)}' relative to ${TypeUtils.toString(containingType)}")
48+
is WildcardType -> type.upperBounds.firstOrNull()
49+
?: error("Unable to unwrap type, wildcard has no upper bound: $type")
50+
is Class<*> -> if (type.isPrimitive) Primitives.wrap(type) else type
51+
else -> error("Unable to unwrap type: $type")
52+
}
53+
}
54+
55+
private fun resolveTypeVariables(type: JavaType, resolving: Set<TypeVariable<*>> = emptySet()): JavaType {
56+
return when (type) {
57+
is ParameterizedType -> {
58+
val actualTypeArguments = type.actualTypeArguments.map { resolveTypeVariables(it, resolving) }.toTypedArray()
59+
ParameterizedTypeImpl(type.rawType as Class<*>, actualTypeArguments, type.ownerType?.let { resolveTypeVariables(it, resolving) })
60+
}
61+
is WildcardType -> TypeUtils.wildcardType()
62+
.withUpperBounds(*type.upperBounds.map { resolveTypeVariables(it, resolving) }.toTypedArray())
63+
.withLowerBounds(*type.lowerBounds.map { resolveTypeVariables(it, resolving) }.toTypedArray())
64+
.build()
65+
is ResolvedType -> {
66+
if (type.typeParameters.isEmpty()) {
67+
type.erasedType
68+
} else {
69+
val actualTypeArguments = type.typeParameters.map { resolveTypeVariables(it) }.toTypedArray()
70+
ParameterizedTypeImpl(type.erasedType, actualTypeArguments, null)
71+
}
72+
}
73+
is TypeVariable<*> -> {
74+
val genericDeclaration = type.genericDeclaration
75+
when {
76+
// only a variable leaked from a raw type can be bound to a type containing itself (e.g. T -> List<T>),
77+
// erase it like the raw type does instead of expanding it forever
78+
type in resolving -> TypeUtils.getRawType(type.bounds.first(), null) ?: Any::class.java
79+
// the containing type binds the variables of all its supertypes
80+
genericDeclaration is Class<*> -> generateSequence(containingType) { (it as? ParameterizedType)?.ownerType }
81+
// an inner class can also use the variables of its outer class, those are bound by its owner type (e.g. Connection<Owner>.Entry)
82+
.firstNotNullOfOrNull { TypeUtils.getTypeArguments(it, genericDeclaration)?.get(type) }
83+
// keep the full type argument (e.g. List<Foo>) rather than its raw class so nested generics aren't lost
84+
?.let { resolveTypeVariables(it, resolving + type) }
85+
?: type
86+
else -> type
87+
}
88+
}
89+
else -> type
90+
}
91+
}
92+
}

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

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,24 @@ internal class SchemaClassScanner(
7878

7979
scanQueue()
8080

81-
// Loop over all objects scanning each one only once for more objects to discover.
82-
do {
83-
do {
84-
// Require all implementors of discovered interfaces to be discovered or provided.
85-
handleDictionaryTypes(getAllObjectTypesImplementingDiscoveredInterfaces()) { "Object type '${it.name}' implements a known interface, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." }
86-
} while (scanQueue())
81+
// Loop over all objects scanning each one only once for more objects to discover. Each step only runs once the steps
82+
// above it have nothing left, and anything new starts over from the top.
83+
while (true) {
84+
// Require all implementors of discovered interfaces to be discovered or provided.
85+
handleDictionaryTypes(getAllObjectTypesImplementingDiscoveredInterfaces()) { "Object type '${it.name}' implements a known interface, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." }
86+
if (scanQueue()) continue
8787

8888
// Require all members of discovered unions to be discovered.
8989
handleDictionaryTypes(getAllObjectTypeMembersOfDiscoveredUnions()) { "Object type '${it.name}' is a member of a known union, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." }
90-
} while (scanQueue())
90+
if (scanQueue()) continue
91+
92+
// Look for unions and interfaces used only by interface fields last, so types that can be reached some other way
93+
// are still discovered in the same order as before. With includeUnusedTypes the unused types loop below already
94+
// picks these up one at a time, scanning in between, so leave that path unchanged.
95+
if (!options.includeUnusedTypes && handleInterfaceFieldTypes()) continue
96+
97+
break
98+
}
9199

92100
handleDirectives()
93101

@@ -278,6 +286,26 @@ internal class SchemaClassScanner(
278286
}.flatten().distinct()
279287
}
280288

289+
/**
290+
* Implementations can narrow an interface field to a member of its union or an implementor of its interface, so such a
291+
* union or interface may not be reachable from any object field. Returns whether any new types were found.
292+
*/
293+
private fun handleInterfaceFieldTypes(): Boolean {
294+
val newTypes = dictionary.keys.filterIsInstance<InterfaceTypeDefinition>().flatMap { iface ->
295+
iface.fieldDefinitions.mapNotNull { field ->
296+
val type = definitionsByName[(field.type.unwrap() as TypeName).name]
297+
if ((type is UnionTypeDefinition || type is InterfaceTypeDefinition) && !dictionary.containsKey(type)) {
298+
type to FieldTypeReference("${iface.name}.${field.name}")
299+
} else {
300+
null
301+
}
302+
}
303+
}
304+
305+
newTypes.forEach { (type, reference) -> handleFoundType(type, null, reference) }
306+
return newTypes.isNotEmpty()
307+
}
308+
281309
private fun handleDictionaryTypes(types: List<TypeDefinition<*>>, failureMessage: (TypeDefinition<*>) -> String) {
282310
types.forEach { type ->
283311
val dictionaryContainsType = dictionary.filter { it.key.name == type.name }.isNotEmpty()
@@ -415,7 +443,7 @@ internal class SchemaClassScanner(
415443
// a class from the dictionary is for the unwrapped type, not for its list or non-null wrapper
416444
if (propertyType != null) inputValueDefinition.type else inputGraphQLType,
417445
inputValueJavaType,
418-
GenericType(genericContext, options).relativeToType(genericContext),
446+
GenericTypeResolver(genericContext, options),
419447
InputObjectReference(inputValueDefinition)
420448
)))
421449
} else {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import java.util.concurrent.Future
2020
import kotlin.coroutines.CoroutineContext
2121
import kotlin.reflect.KClass
2222

23+
@ExposedCopyVisibility
2324
data class SchemaParserOptions internal constructor(
2425
val contextClass: Class<*>?,
2526
val genericWrappers: List<GenericWrapper>,

0 commit comments

Comments
 (0)