Skip to content

Commit b4ba871

Browse files
committed
2 parents cae530a + 6ec3320 commit b4ba871

File tree

12 files changed

+430
-150
lines changed

12 files changed

+430
-150
lines changed

.github/ISSUE_TEMPLATE/bug.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
name: Bug
3+
about: Have you found and confirmed a bug? Report it here.
4+
title: ''
5+
labels: bug
6+
assignees: ''
7+
---
8+
9+
<!-- ⚠️ Please ensure you are reporting the bug in the correct repository! -->
10+
11+
## Description
12+
13+
<!-- Please provide a brief description of the bug -->
14+
15+
### Expected behavior
16+
17+
<!-- Please describe what you expect to happen -->
18+
19+
### Actual behavior
20+
21+
<!-- Please describe what is actually happening -->
22+
23+
### Steps to reproduce the bug
24+
25+
<!-- Please provide the steps to reproduce the issue -->
26+
27+
1. ...
28+
2. ...
29+
3. ...
30+

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
blank_issues_enabled: false
2+
contact_links:
3+
- name: Question
4+
url: https://spectrum.chat/graphql-java-kick
5+
about: Anything you are not sure about? Ask the community on Spectrum!

.github/ISSUE_TEMPLATE/feature.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
name: Feature
3+
about: Suggest an idea for a new feature.
4+
title: ''
5+
labels: enhancement
6+
assignees: ''
7+
---
8+
9+
<!-- ⚠️ Please ensure you are requesting the feature in the correct repository! -->
10+
11+
__I want to suggest an idea and checked that ...__
12+
13+
- [ ] ... to my best knowledge, my idea wouldn't break something for other users
14+
- [ ] ... the documentation does not mention anything about my idea
15+
- [ ] ... there are no open or closed issues that are related to my idea
16+
17+
## Description
18+
19+
<!-- Please provide a brief description of the feature -->
20+
21+
### Use Cases
22+
23+
<!-- Please describe how your suggestion would benefit you and other users -->

README.md

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,52 @@
22

33
[![TravisCI Build](https://travis-ci.org/graphql-java-kickstart/graphql-java-tools.svg?branch=master)](https://travis-ci.org/graphql-java-kickstart/graphql-java-tools)
44
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.graphql-java-kickstart/graphql-java-tools/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java-kickstart/graphql-java-tools)
5-
[![Chat on Slack](https://img.shields.io/badge/slack-join%20chat-informational)](https://graphqljavakickstart.slack.com)
5+
[![Chat on Spectrum](https://img.shields.io/badge/spectrum-join%20the%20community-%23800080)](https://spectrum.chat/graphql-java-kick)
66

77
This library allows you to use the GraphQL schema language to build your [graphql-java](https://github.com/graphql-java/graphql-java) schema.
88
Inspired by [graphql-tools](https://github.com/apollographql/graphql-tools), it parses the given GraphQL schema and allows you to BYOO (bring your own object) to fill in the implementations.
9-
GraphQL Java Tools works extremely well if you already have domain POJOs that hold your data (e.g. for RPC, ORM, REST, etc) by allowing you to map these magically to GraphQL objects.
9+
GraphQL Java Tools works well if you already have domain POJOs that hold your data (e.g. for RPC, ORM, REST, etc) by allowing you to map these "magically" to GraphQL objects.
1010

1111
GraphQL Java Tools aims for seamless integration with Java, but works for any JVM language. Try it with Kotlin!
1212

13-
## WARNING: NoClassDefFoundError when using Spring Boot
13+
## We are looking for contributors!
14+
Are you interested in improving our documentation, working on the codebase, reviewing PRs?
1415

15-
If you're using `graphl-java-tools` with Spring Boot version lower than 2.2 you need to set the `kotlin.version` in
16-
your Spring Boot project explicitly to version 1.3.70, because Spring Boot Starter parent currently overrides it with
17-
a 1.2.* version of Kotlin.
18-
`graphql-java-tools` requires 1.3.* however because of its coroutine support. If you don't override this version
19-
you will run into a `NoClassDefFoundError`.
16+
[Reach out to us on Spectrum](https://spectrum.chat/graphql-java-kick) and join the team!
2017

21-
Spring Boot team has indicated the Kotlin version will be upgraded to 1.3 in Spring Boot 2.2.
18+
## Quick start
2219

2320
### Using Gradle
24-
Set the Kotlin version in your `gradle.properties`
21+
Set the Kotlin version in your `gradle.properties`:
2522
```
2623
kotlin.version=1.3.70
2724
```
2825

26+
Add the dependency:
27+
```groovy
28+
compile 'com.graphql-java-kickstart:graphql-java-tools:6.2.0'
29+
```
30+
2931
### Using Maven
30-
Set the Kotlin version in your `<properties>` section
32+
Set the Kotlin version in your `<properties>` section:
3133
```xml
3234
<properties>
3335
<kotlin.version>1.3.70</kotlin.version>
3436
</properties>
3537
```
3638

39+
Add the dependency:
40+
```xml
41+
<dependency>
42+
<groupId>com.graphql-java-kickstart</groupId>
43+
<artifactId>graphql-java-tools</artifactId>
44+
<version>6.2.0</version>
45+
</dependency>
46+
```
47+
3748
## Documentation
3849

39-
Take a look at our new [documentation](https://www.graphql-java-kickstart.com/tools/) for more details.
50+
Take a look at our [documentation](https://www.graphql-java-kickstart.com/tools/) for more details.
4051

4152
## Why GraphQL Java Tools?
4253

@@ -49,31 +60,12 @@ A few libraries exist to ease the boilerplate pain, including [GraphQL-Java's bu
4960
* **Class Validation**: Since there aren't any compile-time checks of the type->class relationship, GraphQL Java Tools will warn you if you provide classes/types that you don't need to, as well as erroring if you use the wrong Java class for a certain GraphQL type when it builds the schema.
5061
* **Unit Testing**: Since your GraphQL schema is independent of your data model, this makes your classes simple and extremely testable.
5162

52-
## Build with Maven or Gradle
63+
## WARNING: NoClassDefFoundError when using Spring Boot
5364

54-
```xml
55-
<dependency>
56-
<groupId>com.graphql-java-kickstart</groupId>
57-
<artifactId>graphql-java-tools</artifactId>
58-
<version>6.2.0</version>
59-
</dependency>
60-
```
61-
```groovy
62-
compile 'com.graphql-java-kickstart:graphql-java-tools:6.0.2'
63-
```
65+
If you're using `graphl-java-tools` with Spring Boot version lower than 2.2 you need to set the `kotlin.version` in
66+
your Spring Boot project explicitly to version 1.3.70, because Spring Boot Starter parent currently overrides it with
67+
a 1.2.* version of Kotlin.
68+
`graphql-java-tools` requires 1.3.* however because of its coroutine support. If you don't override this version
69+
you will run into a `NoClassDefFoundError`.
6470

65-
New releases will be available faster in the JCenter repository than in Maven Central. Add the following to use for Maven
66-
```xml
67-
<repositories>
68-
<repository>
69-
<id>jcenter</id>
70-
<url>https://jcenter.bintray.com/</url>
71-
</repository>
72-
</repositories>
73-
```
74-
For Gradle:
75-
```groovy
76-
repositories {
77-
jcenter()
78-
}
79-
```
71+
Spring Boot team has indicated the Kotlin version will be upgraded to 1.3 in Spring Boot 2.2.

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,30 @@ internal class SchemaClassScanner(
7777
do {
7878
do {
7979
// Require all implementors of discovered interfaces to be discovered or provided.
80-
handleInterfaceOrUnionSubTypes(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." }
80+
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." }
8181
} while (scanQueue())
8282

8383
// Require all members of discovered unions to be discovered.
84-
handleInterfaceOrUnionSubTypes(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." }
84+
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." }
8585
} while (scanQueue())
8686

87+
// Find unused types and include them if required
88+
if (options.includeUnusedTypes) {
89+
do {
90+
val unusedDefinitions = (definitionsByName.values - (dictionary.keys.toSet() + unvalidatedTypes))
91+
.filter { definition -> definition.name != "PageInfo" }
92+
.filterIsInstance<ObjectTypeDefinition>().distinct()
93+
94+
if (unusedDefinitions.isEmpty()) {
95+
break
96+
}
97+
98+
val unusedDefinition = unusedDefinitions.first()
99+
100+
handleDictionaryTypes(listOf(unusedDefinition)) { "Object type '${it.name}' is unused and includeUnusedTypes is true. Please pass a class for type '${it.name}' in the parser's dictionary." }
101+
} while (scanQueue())
102+
}
103+
87104
return validateAndCreateResult(rootTypeHolder)
88105
}
89106

@@ -208,7 +225,7 @@ internal class SchemaClassScanner(
208225
}.flatten().distinct()
209226
}
210227

211-
private fun handleInterfaceOrUnionSubTypes(types: List<ObjectTypeDefinition>, failureMessage: (ObjectTypeDefinition) -> String) {
228+
private fun handleDictionaryTypes(types: List<ObjectTypeDefinition>, failureMessage: (ObjectTypeDefinition) -> String) {
212229
types.forEach { type ->
213230
val dictionaryContainsType = dictionary.filter { it.key.name == type.name }.isNotEmpty()
214231
if (!unvalidatedTypes.contains(type) && !dictionaryContainsType) {

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ data class SchemaParserOptions internal constructor(
2929
val introspectionEnabled: Boolean,
3030
val coroutineContextProvider: CoroutineContextProvider,
3131
val typeDefinitionFactories: List<TypeDefinitionFactory>,
32-
val fieldVisibility: GraphqlFieldVisibility?
32+
val fieldVisibility: GraphqlFieldVisibility?,
33+
val includeUnusedTypes: Boolean
3334
) {
3435
companion object {
3536
@JvmStatic
@@ -56,6 +57,7 @@ data class SchemaParserOptions internal constructor(
5657
private var coroutineContextProvider: CoroutineContextProvider? = null
5758
private var typeDefinitionFactories: MutableList<TypeDefinitionFactory> = mutableListOf(RelayConnectionFactory())
5859
private var fieldVisibility: GraphqlFieldVisibility? = null
60+
private var includeUnusedTypes = false
5961

6062
fun contextClass(contextClass: Class<*>) = this.apply {
6163
this.contextClass = contextClass
@@ -125,6 +127,10 @@ data class SchemaParserOptions internal constructor(
125127
this.fieldVisibility = fieldVisibility
126128
}
127129

130+
fun includeUnusedTypes(includeUnusedTypes: Boolean) = this.apply {
131+
this.includeUnusedTypes = includeUnusedTypes
132+
}
133+
128134
@ExperimentalCoroutinesApi
129135
fun build(): SchemaParserOptions {
130136
val coroutineContextProvider = coroutineContextProvider
@@ -162,7 +168,8 @@ data class SchemaParserOptions internal constructor(
162168
introspectionEnabled,
163169
coroutineContextProvider,
164170
typeDefinitionFactories,
165-
fieldVisibility
171+
fieldVisibility,
172+
includeUnusedTypes
166173
)
167174
}
168175
}

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

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import graphql.schema.DataFetchingEnvironment
1414
import graphql.schema.GraphQLTypeUtil.isScalar
1515
import kotlinx.coroutines.future.future
1616
import java.lang.reflect.Method
17-
import java.lang.reflect.ParameterizedType
18-
import java.lang.reflect.WildcardType
1917
import java.util.*
2018
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
2119
import kotlin.reflect.full.valueParameters
@@ -82,15 +80,13 @@ internal class MethodFieldResolver(
8280
return@add Optional.empty<Any>()
8381
}
8482

85-
if (value != null
86-
&& parameterType.unwrap().isAssignableFrom(value.javaClass)
87-
&& isScalarType(environment, definition.type, parameterType)) {
88-
return@add value
83+
if (value == null || shouldValueBeConverted(value, definition, parameterType, environment)) {
84+
return@add mapper.convertValue(value, object : TypeReference<Any>() {
85+
override fun getType() = parameterType
86+
})
8987
}
9088

91-
return@add mapper.convertValue(value, object : TypeReference<Any>() {
92-
override fun getType() = parameterType
93-
})
89+
return@add value
9490
}
9591
}
9692

@@ -110,22 +106,25 @@ internal class MethodFieldResolver(
110106
}
111107
}
112108

113-
private fun isScalarType(environment: DataFetchingEnvironment, type: Type<*>, genericParameterType: JavaType): Boolean =
114-
when (type) {
109+
private fun shouldValueBeConverted(value: Any, definition: InputValueDefinition, parameterType: JavaType, environment: DataFetchingEnvironment): Boolean {
110+
return !parameterType.unwrap().isAssignableFrom(value.javaClass) || !isConcreteScalarType(environment, definition.type, parameterType)
111+
}
112+
113+
/**
114+
* A concrete scalar type is a scalar type where values always coerce to the same Java type. The ID scalar type is not concrete
115+
* because values can be coerced to multiple different Java types (eg. String, Long, UUID). All values of a non-concrete scalar
116+
* type must be converted to the target method parameter type.
117+
*/
118+
private fun isConcreteScalarType(environment: DataFetchingEnvironment, type: Type<*>, genericParameterType: JavaType): Boolean {
119+
return when (type) {
115120
is ListType -> List::class.java.isAssignableFrom(this.genericType.getRawClass(genericParameterType))
116-
&& isScalarType(environment, type.type, this.genericType.unwrapGenericType(genericParameterType))
117-
is TypeName -> environment.graphQLSchema?.getType(type.name)?.let { isScalar(it) && !isJavaLanguageType(genericParameterType) }
121+
&& isConcreteScalarType(environment, type.type, this.genericType.unwrapGenericType(genericParameterType))
122+
is TypeName -> environment.graphQLSchema?.getType(type.name)?.let { isScalar(it) && type.name != "ID" }
118123
?: false
119-
is NonNullType -> isScalarType(environment, type.type, genericParameterType)
124+
is NonNullType -> isConcreteScalarType(environment, type.type, genericParameterType)
120125
else -> false
121126
}
122-
123-
private fun isJavaLanguageType(type: JavaType): Boolean =
124-
when (type) {
125-
is ParameterizedType -> isJavaLanguageType(type.actualTypeArguments[0])
126-
is WildcardType -> isJavaLanguageType(type.upperBounds[0])
127-
else -> genericType.getRawClass(type).`package`.name == "java.lang"
128-
}
127+
}
129128

130129
override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> {
131130
val unwrappedGenericType = genericType.unwrapGenericType(try {

0 commit comments

Comments
 (0)