Skip to content

Commit 18a5f9b

Browse files
smyrickdariuszkuc
authored andcommitted
Support List for input arguments (#546)
* Support List for input arguments Fixes #509 Adds support for using List<*> as the type for function arguments instead of the concrete class ArrayList or arrays * Remove unused unit test We don't cache values in the generateUnion method anymore so we don't need to validate the cache there * Remove unused exception: * Update unit tests for coverage levels
1 parent 81c00d5 commit 18a5f9b

File tree

14 files changed

+201
-134
lines changed

14 files changed

+201
-134
lines changed

docs/writing-schemas/lists.md

Lines changed: 55 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,55 @@
1-
---
2-
id: lists
3-
title: Lists
4-
---
5-
6-
Both `kotlin.Array` and `kotlin.collections.List` are automatically mapped to the GraphQL `List` type (for unsupported
7-
use cases see below). Type arguments provided to Kotlin collections are used as the type arguments in the GraphQL `List`
8-
type. Kotlin specialized classes representing arrays of Java primitive types without boxing overhead (e.g. `IntArray`)
9-
are also supported.
10-
11-
```kotlin
12-
class SimpleQuery {
13-
fun generateList(): List<Int> {
14-
// some logic here that generates list
15-
}
16-
17-
fun doSomethingWithIntArray(ints: IntArray): String {
18-
// some logic here that processes array
19-
}
20-
```
21-
22-
The above Kotlin class would produce the following GraphQL schema:
23-
24-
```graphql
25-
schema {
26-
query: Query
27-
}
28-
29-
type Query {
30-
generateList: [Int!]!
31-
doSomethingWithIntArray(ints: [Int!]!): String!
32-
}
33-
```
34-
35-
### Primitive Arrays
36-
37-
`graphql-kotlin-schema-generator` supports the following primitive array types without autoboxing overhead. Similarly to
38-
the `kotlin.Array` of objects the underlying type is automatically mapped to GraphQL `List` type.
39-
40-
| Kotlin Type |
41-
|-----------------------|
42-
| `kotlin.IntArray` | | `kotlin.LongArray` | | `kotlin.ShortArray` | | `kotlin.FloatArray` | |
43-
`kotlin.DoubleArray` | | `kotlin.CharArray` | | `kotlin.BooleanArray` |
44-
45-
> NOTE: Underlying GraphQL types of primitive arrays will be corresponding to the built-in scalar types or extended
46-
> scalar types provided by `graphql-java`.
47-
48-
### Unsupported Collection Types
49-
50-
Currently GraphQL spec only supports `Lists`. Therefore even though Java and Kotlin support number of other collection
51-
types, `graphql-kotlin-schema-generator` only explicitly supports `Lists` and primitive arrays. Other collection types
52-
such as `Sets` (see [#201](https://github.com/ExpediaGroup/graphql-kotlin/issues/201)) and arbitrary `Map` data
53-
structures are not supported.
54-
55-
Additionally, GraphQL spec does not allow interfaces/union types to be used for a query input arguments which means you
56-
can only use lists of concrete GraphQL types as your query input arguments.
1+
---
2+
id: lists
3+
title: Lists
4+
---
5+
6+
Both `kotlin.Array` and `kotlin.collections.List` are automatically mapped to the GraphQL `List` type (for unsupported
7+
use cases see below). Type arguments provided to Kotlin collections are used as the type arguments in the GraphQL `List`
8+
type. Kotlin specialized classes representing arrays of Java primitive types without boxing overhead (e.g. `IntArray`)
9+
are also supported.
10+
11+
```kotlin
12+
class SimpleQuery {
13+
fun generateList(): List<Int> {
14+
// some logic here that generates list
15+
}
16+
17+
fun doSomethingWithIntArray(ints: IntArray): String {
18+
// some logic here that processes array
19+
}
20+
21+
fun doSomethingWithIntList(ints: List<Int>): String {
22+
// some logic here that processes list
23+
}
24+
}
25+
```
26+
27+
The above Kotlin class would produce the following GraphQL schema:
28+
29+
```graphql
30+
type Query {
31+
generateList: [Int!]!
32+
doSomethingWithIntArray(ints: [Int!]!): String!
33+
doSomethingWithIntList(ints: [Int!]!): String!
34+
}
35+
```
36+
37+
### Primitive Arrays
38+
39+
`graphql-kotlin-schema-generator` supports the following primitive array types without autoboxing overhead. Similarly to
40+
the `kotlin.Array` of objects the underlying type is automatically mapped to GraphQL `List` type.
41+
42+
| Kotlin Type |
43+
|-----------------------|
44+
| `kotlin.IntArray` | | `kotlin.LongArray` | | `kotlin.ShortArray` | | `kotlin.FloatArray` | |
45+
`kotlin.DoubleArray` | | `kotlin.CharArray` | | `kotlin.BooleanArray` |
46+
47+
> NOTE: Underlying GraphQL types of primitive arrays will be corresponding to the built-in scalar types or extended
48+
> scalar types provided by `graphql-java`.
49+
50+
### Unsupported Collection Types
51+
52+
Currently GraphQL spec only supports `Lists`. Therefore even though Java and Kotlin support number of other collection
53+
types, `graphql-kotlin-schema-generator` only explicitly supports `Lists` and primitive arrays. Other collection types
54+
such as `Sets` (see [#201](https://github.com/ExpediaGroup/graphql-kotlin/issues/201)) and arbitrary `Map` data
55+
structures are not supported.

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/CouldNotCastArgumentException.kt

Lines changed: 0 additions & 25 deletions
This file was deleted.

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kParameterExtensions.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@
1717
package com.expediagroup.graphql.generator.extensions
1818

1919
import com.expediagroup.graphql.annotations.GraphQLContext
20-
import com.expediagroup.graphql.exceptions.CouldNotCastArgumentException
2120
import com.expediagroup.graphql.exceptions.CouldNotGetNameOfKParameterException
2221
import graphql.schema.DataFetchingEnvironment
2322
import kotlin.reflect.KParameter
2423
import kotlin.reflect.full.findAnnotation
24+
import kotlin.reflect.full.isSubclassOf
2525
import kotlin.reflect.jvm.javaType
2626

2727
internal fun KParameter.isInterface() = this.type.getKClass().isInterface()
2828

29+
internal fun KParameter.isList() = this.type.getKClass().isSubclassOf(List::class)
30+
2931
internal fun KParameter.isGraphQLContext() = this.findAnnotation<GraphQLContext>() != null
3032

3133
internal fun KParameter.isDataFetchingEnvironment() = this.type.classifier == DataFetchingEnvironment::class
@@ -34,6 +36,9 @@ internal fun KParameter.isDataFetchingEnvironment() = this.type.classifier == Da
3436
internal fun KParameter.getName(): String =
3537
this.getGraphQLName() ?: this.name ?: throw CouldNotGetNameOfKParameterException(this)
3638

37-
@Throws(CouldNotCastArgumentException::class)
3839
internal fun KParameter.javaTypeClass(): Class<*> =
39-
this.type.javaType as? Class<*> ?: throw CouldNotCastArgumentException(this)
40+
if (this.isList()) {
41+
this.type.getKClass().java
42+
} else {
43+
this.type.javaType as Class<*>
44+
}

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/state/TypesCache.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,6 @@ internal class TypesCache(private val supportedPackages: List<String>) {
7070
internal fun doesNotContainGraphQLType(graphQLType: GraphQLType) =
7171
cache.none { (_, v) -> v.graphQLType.name == graphQLType.name }
7272

73-
internal fun doesNotContain(kClass: KClass<*>): Boolean = cache.none { (_, ktype) -> ktype.kClass == kClass }
74-
7573
/**
7674
* We do not want to cache list types since it is just a simple wrapper.
7775
* Enums do not have a different name for input and output.

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/generateArgument.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ import com.expediagroup.graphql.generator.extensions.getGraphQLDescription
2222
import com.expediagroup.graphql.generator.extensions.getName
2323
import com.expediagroup.graphql.generator.extensions.isGraphQLID
2424
import com.expediagroup.graphql.generator.extensions.isInterface
25+
import com.expediagroup.graphql.generator.extensions.isList
2526
import com.expediagroup.graphql.generator.extensions.safeCast
2627
import graphql.schema.GraphQLArgument
2728
import kotlin.reflect.KParameter
2829

2930
@Throws(InvalidInputFieldTypeException::class)
3031
internal fun generateArgument(generator: SchemaGenerator, parameter: KParameter): GraphQLArgument {
3132

32-
if (parameter.isInterface()) {
33+
if (parameter.isInterface() && parameter.isList().not()) {
3334
throw InvalidInputFieldTypeException(parameter)
3435
}
3536

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcherTest.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package com.expediagroup.graphql.execution
1818

1919
import com.expediagroup.graphql.annotations.GraphQLContext
2020
import com.expediagroup.graphql.annotations.GraphQLName
21-
import com.expediagroup.graphql.exceptions.CouldNotCastArgumentException
2221
import com.fasterxml.jackson.annotation.JsonProperty
2322
import graphql.GraphQLException
2423
import graphql.schema.DataFetchingEnvironment
@@ -28,7 +27,6 @@ import kotlinx.coroutines.coroutineScope
2827
import org.junit.jupiter.api.Test
2928
import java.util.concurrent.CompletableFuture
3029
import kotlin.test.assertEquals
31-
import kotlin.test.assertFailsWith
3230
import kotlin.test.assertFalse
3331
import kotlin.test.assertNotNull
3432
import kotlin.test.assertNull
@@ -110,14 +108,12 @@ internal class FunctionDataFetcherTest {
110108
}
111109

112110
@Test
113-
fun `list inputs throws exception`() {
111+
fun `list can be converted by the object mapper`() {
114112
val dataFetcher = FunctionDataFetcher(target = MyClass(), fn = MyClass::printList)
115113
val mockEnvironmet: DataFetchingEnvironment = mockk()
116114
every { mockEnvironmet.arguments } returns mapOf("items" to listOf("foo", "bar"))
117115

118-
assertFailsWith(CouldNotCastArgumentException::class) {
119-
dataFetcher.get(mockEnvironmet)
120-
}
116+
assertEquals(expected = "foo:bar", actual = dataFetcher.get(mockEnvironmet))
121117
}
122118

123119
@Test

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/extensions/GraphQLSchemaExtensionsTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ class GraphQLSchemaExtensionsTest {
218218
}
219219

220220
@GraphQLDirective(locations = [Introspection.DirectiveLocation.FIELD_DEFINITION])
221-
internal annotation class CustomDirective
221+
annotation class CustomDirective
222222

223223
class QueryWithDirectives {
224224
fun echo(msg: String): String = msg

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/ToSchemaTest.kt

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import kotlin.test.assertTrue
4949
"Detekt.LargeClass",
5050
"Detekt.MethodOverloading")
5151
class ToSchemaTest {
52+
5253
@Test
5354
fun `SchemaGenerator generates a simple GraphQL schema`() {
5455
val schema = toSchema(
@@ -82,21 +83,26 @@ class ToSchemaTest {
8283
}
8384

8485
@Test
85-
fun `Schema generator exposes arrays of primitive types as function arguments`() {
86-
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithArray())), config = testSchemaConfig)
86+
fun `Schema generator exposes arrays and lists as function arguments`() {
87+
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithLists())), config = testSchemaConfig)
8788
val firstArgumentType = schema.queryType.getFieldDefinition("sumOf").arguments[0].type.deepName
8889
assertEquals("[Int!]!", firstArgumentType)
90+
val secondArgumentType = schema.queryType.getFieldDefinition("sumOfList").arguments[0].type.deepName
91+
assertEquals("[Int!]!", secondArgumentType)
8992

9093
val graphQL = GraphQL.newGraphQL(schema).build()
91-
val result = graphQL.execute("{ sumOf(ints: [1, 2, 3]) }")
92-
val sum = result.getData<Map<String, Int>>().values.first()
94+
val arrayResult = graphQL.execute("{ sumOf(ints: [1, 2, 3]) }")
95+
val arraySum = arrayResult.getData<Map<String, Int>>().values.first()
96+
assertEquals(6, arraySum)
9397

94-
assertEquals(6, sum)
98+
val listResult = graphQL.execute("{ sumOfList(ints: [1, 2, 3]) }")
99+
val listSum = listResult.getData<Map<String, Int>>().values.first()
100+
assertEquals(6, listSum)
95101
}
96102

97103
@Test
98104
fun `Schema generator exposes arrays of complex types as function arguments`() {
99-
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithArray())), config = testSchemaConfig)
105+
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithLists())), config = testSchemaConfig)
100106
val firstArgumentType = schema.queryType.getFieldDefinition("sumOfComplexArray").arguments[0].type.deepName
101107
assertEquals("[ComplexWrappingTypeInput!]!", firstArgumentType)
102108

@@ -342,9 +348,10 @@ class ToSchemaTest {
342348
fun query(@GraphQLDescription("A GraphQL value") value: Int): Geography = Geography(value, GeoType.CITY, listOf())
343349
}
344350

345-
class QueryWithArray {
351+
class QueryWithLists {
346352
fun sumOf(ints: IntArray): Int = ints.sum()
347353
fun sumOfComplexArray(objects: Array<ComplexWrappingType>): Int = objects.map { it.value }.sum()
354+
fun sumOfList(ints: List<Int>): Int = ints.sum()
348355
}
349356

350357
class QueryWithIgnored {

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/extensions/KParameterExtensionsKtTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ internal class KParameterExtensionsKtTest {
5252

5353
internal fun absctractInput(myAbstractClass: MyAbstractClass) = myAbstractClass
5454

55+
internal fun listInput(myList: List<Int>) = myList
56+
57+
internal fun arrayInput(myArray: IntArray) = myArray
58+
5559
internal fun noDescription(myClass: MyClass) = myClass
5660

5761
internal fun paramDescription(@GraphQLDescription("param description") myClass: MyClass) = myClass
@@ -124,5 +128,14 @@ internal class KParameterExtensionsKtTest {
124128
@Test
125129
fun javaTypeClass() {
126130
assertEquals(expected = String::class.java, actual = MyKotlinClass::stringFun.findParameterByName("string")?.javaTypeClass())
131+
assertEquals(expected = List::class.java, actual = Container::listInput.findParameterByName("myList")?.javaTypeClass())
132+
assertEquals(expected = MyInterface::class.java, actual = Container::interfaceInput.findParameterByName("myInterface")?.javaTypeClass())
133+
}
134+
135+
@Test
136+
fun isList() {
137+
assertTrue(Container::listInput.findParameterByName("myList")?.isList() == true)
138+
assertTrue(Container::arrayInput.findParameterByName("myArray")?.isList() == false)
139+
assertTrue(Container::interfaceInput.findParameterByName("myInterface")?.isList() == false)
127140
}
128141
}

graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/GenerateArgumentTest.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import com.expediagroup.graphql.annotations.GraphQLName
2222
import com.expediagroup.graphql.exceptions.InvalidInputFieldTypeException
2323
import com.expediagroup.graphql.test.utils.SimpleDirective
2424
import graphql.Scalars
25+
import graphql.schema.GraphQLList
2526
import graphql.schema.GraphQLNonNull
27+
import graphql.schema.GraphQLTypeUtil
2628
import org.junit.jupiter.api.Test
2729
import kotlin.reflect.full.findParameterByName
2830
import kotlin.test.assertEquals
@@ -45,6 +47,12 @@ internal class GenerateArgumentTest : TypeTestHelper() {
4547
fun id(@GraphQLID idArg: String) = "Your id is $idArg"
4648

4749
fun interfaceArg(input: MyInterface) = input.id
50+
51+
fun arrayArg(input: IntArray) = input
52+
53+
fun arrayListArg(input: ArrayList<String>) = input
54+
55+
fun listArg(input: List<String>) = input
4856
}
4957

5058
@Test
@@ -97,4 +105,34 @@ internal class GenerateArgumentTest : TypeTestHelper() {
97105
generateArgument(generator, kParameter)
98106
}
99107
}
108+
109+
@Test
110+
fun `Primitive array argument type is valid`() {
111+
val kParameter = ArgumentTestClass::arrayArg.findParameterByName("input")
112+
assertNotNull(kParameter)
113+
val result = generateArgument(generator, kParameter)
114+
115+
assertEquals(expected = "input", actual = result.name)
116+
assertNotNull(GraphQLTypeUtil.unwrapNonNull(result.type) as? GraphQLList)
117+
}
118+
119+
@Test
120+
fun `ArrayList argument type is valid`() {
121+
val kParameter = ArgumentTestClass::arrayListArg.findParameterByName("input")
122+
assertNotNull(kParameter)
123+
val result = generateArgument(generator, kParameter)
124+
125+
assertEquals(expected = "input", actual = result.name)
126+
assertNotNull(GraphQLTypeUtil.unwrapNonNull(result.type) as? GraphQLList)
127+
}
128+
129+
@Test
130+
fun `List argument type is valid`() {
131+
val kParameter = ArgumentTestClass::listArg.findParameterByName("input")
132+
assertNotNull(kParameter)
133+
val result = generateArgument(generator, kParameter)
134+
135+
assertEquals(expected = "input", actual = result.name)
136+
assertNotNull(GraphQLTypeUtil.unwrapNonNull(result.type) as? GraphQLList)
137+
}
100138
}

0 commit comments

Comments
 (0)