Skip to content

Commit 4492e9f

Browse files
smyrickShane Myrick
and
Shane Myrick
authored
Custom scalar input (ExpediaGroup#636)
* Add more tests for custom scalar input * Undo api changes * Fix input for lists of custom scalars Converting a list input type for custom scalars was losing the type information. We need to use a specific Jackson conversion method to properly convert the List interface * Remove reference to external classes in javadoc Co-authored-by: Shane Myrick <[email protected]>
1 parent 8089c8e commit 4492e9f

File tree

8 files changed

+157
-38
lines changed

8 files changed

+157
-38
lines changed

examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/ScalarQuery.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class ScalarQuery : Query {
3232
@GraphQLDescription("generates random UUID")
3333
fun generateRandomUUID() = UUID.randomUUID()
3434

35+
@GraphQLDescription("Prints a string with a custom scalar as input")
36+
fun printUuids(uuids: List<UUID>) = "You sent $uuids"
37+
3538
fun findPersonById(@GraphQLID id: String) = Person(id, "Nelson")
3639
}
3740

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcher.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
package com.expediagroup.graphql.execution
1818

19+
import com.expediagroup.graphql.generator.extensions.getJavaClass
1920
import com.expediagroup.graphql.generator.extensions.getName
21+
import com.expediagroup.graphql.generator.extensions.getTypeOfFirstArgument
2022
import com.expediagroup.graphql.generator.extensions.isDataFetchingEnvironment
2123
import com.expediagroup.graphql.generator.extensions.isGraphQLContext
22-
import com.expediagroup.graphql.generator.extensions.javaTypeClass
24+
import com.expediagroup.graphql.generator.extensions.isList
2325
import com.fasterxml.jackson.databind.ObjectMapper
2426
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
2527
import graphql.schema.DataFetcher
@@ -95,14 +97,20 @@ open class FunctionDataFetcher(
9597
/**
9698
* Called to convert the generic input object to the parameter class.
9799
*
98-
* This is currently achieved by using a Jackson [ObjectMapper].
100+
* This is currently achieved by using a Jackson ObjectMapper.
99101
*/
100102
protected open fun convertParameterValue(param: KParameter, environment: DataFetchingEnvironment): Any? {
101103
val name = param.getName()
102-
val klazz = param.javaTypeClass()
103104
val argument = environment.arguments[name]
104105

105-
return objectMapper.convertValue(argument, klazz)
106+
return if (param.isList()) {
107+
val argumentClass = param.type.getTypeOfFirstArgument().getJavaClass()
108+
val jacksonCollectionType = objectMapper.typeFactory.constructCollectionType(List::class.java, argumentClass)
109+
objectMapper.convertValue(argument, jacksonCollectionType)
110+
} else {
111+
val javaClass = param.type.getJavaClass()
112+
objectMapper.convertValue(argument, javaClass)
113+
}
106114
}
107115

108116
/**

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import graphql.schema.DataFetchingEnvironment
2222
import kotlin.reflect.KParameter
2323
import kotlin.reflect.full.findAnnotation
2424
import kotlin.reflect.full.isSubclassOf
25-
import kotlin.reflect.jvm.javaType
2625

2726
internal fun KParameter.isInterface() = this.type.getKClass().isInterface()
2827

@@ -35,10 +34,3 @@ internal fun KParameter.isDataFetchingEnvironment() = this.type.classifier == Da
3534
@Throws(CouldNotGetNameOfKParameterException::class)
3635
internal fun KParameter.getName(): String =
3736
this.getGraphQLName() ?: this.name ?: throw CouldNotGetNameOfKParameterException(this)
38-
39-
internal fun KParameter.javaTypeClass(): Class<*> =
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/extensions/kTypeExtensions.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import kotlin.reflect.KClass
2121
import kotlin.reflect.KType
2222
import kotlin.reflect.full.createType
2323
import kotlin.reflect.full.isSubclassOf
24+
import kotlin.reflect.jvm.javaType
2425
import kotlin.reflect.jvm.jvmErasure
2526

2627
private val primitiveArrayTypes = mapOf(
@@ -35,6 +36,8 @@ private val primitiveArrayTypes = mapOf(
3536

3637
internal fun KType.getKClass() = this.jvmErasure
3738

39+
internal fun KType.getJavaClass() = this.javaType as Class<*>
40+
3841
internal fun KType.isSubclassOf(kClass: KClass<*>) = this.getKClass().isSubclassOf(kClass)
3942

4043
@Throws(InvalidListTypeException::class)

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,6 @@ internal class KParameterExtensionsKtTest {
125125
assertFalse(param?.isDataFetchingEnvironment().isTrue())
126126
}
127127

128-
@Test
129-
fun javaTypeClass() {
130-
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-
135128
@Test
136129
fun isList() {
137130
assertTrue(Container::listInput.findParameterByName("myList")?.isList() == true)

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

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ import com.expediagroup.graphql.exceptions.EmptyInterfaceTypeException
2323
import com.expediagroup.graphql.exceptions.EmptyObjectTypeException
2424
import com.expediagroup.graphql.generator.extensions.getSimpleName
2525
import com.expediagroup.graphql.getTestSchemaConfigWithHooks
26+
import com.expediagroup.graphql.test.utils.graphqlUUIDType
2627
import com.expediagroup.graphql.testSchemaConfig
2728
import com.expediagroup.graphql.toSchema
28-
import graphql.language.StringValue
29-
import graphql.schema.Coercing
3029
import graphql.schema.GraphQLFieldDefinition
3130
import graphql.schema.GraphQLInterfaceType
3231
import graphql.schema.GraphQLObjectType
@@ -328,21 +327,4 @@ class SchemaGeneratorHooksTest {
328327
}
329328

330329
class EmptyImplementation(override val id: String) : EmptyInterface
331-
332-
private val graphqlUUIDType = GraphQLScalarType.newScalar()
333-
.name("UUID")
334-
.description("A type representing a formatted java.util.UUID")
335-
.coercing(UUIDCoercing)
336-
.build()
337-
338-
private object UUIDCoercing : Coercing<UUID, String> {
339-
override fun parseValue(input: Any?): UUID = UUID.fromString(serialize(input))
340-
341-
override fun parseLiteral(input: Any?): UUID? {
342-
val uuidString = (input as? StringValue)?.value
343-
return UUID.fromString(uuidString)
344-
}
345-
346-
override fun serialize(dataFetcherResult: Any?): String = dataFetcherResult.toString()
347-
}
348330
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.test.integration
18+
19+
import com.expediagroup.graphql.TopLevelObject
20+
import com.expediagroup.graphql.getTestSchemaConfigWithHooks
21+
import com.expediagroup.graphql.hooks.SchemaGeneratorHooks
22+
import com.expediagroup.graphql.test.utils.graphqlUUIDType
23+
import com.expediagroup.graphql.toSchema
24+
import graphql.GraphQL
25+
import graphql.schema.GraphQLType
26+
import org.junit.jupiter.api.Test
27+
import java.util.UUID
28+
import kotlin.reflect.KClass
29+
import kotlin.reflect.KType
30+
import kotlin.test.assertEquals
31+
import kotlin.test.assertFails
32+
import kotlin.test.assertNotNull
33+
34+
class CustomScalarExecutionTest {
35+
36+
private val schema = toSchema(
37+
queries = listOf(TopLevelObject(QueryObject())),
38+
config = getTestSchemaConfigWithHooks(CustomScalarHooks())
39+
)
40+
private val graphQL: GraphQL = GraphQL.newGraphQL(schema).build()
41+
42+
@Test
43+
fun `a custom scalar can be used as output`() {
44+
val result = graphQL.execute("{ uuidOutput }")
45+
val data: Map<String, String>? = result.getData()
46+
47+
assertNotNull(data?.get("uuidOutput"))
48+
}
49+
50+
@Test
51+
fun `a custom scalar can be used as input`() {
52+
val result = graphQL.execute("{ uuidInput(uuid: \"22dba784-f7c1-471f-8e8c-84e3a59bb5b5\") }")
53+
val data: Map<String, String>? = result.getData()
54+
55+
assertNotNull(data?.get("uuidInput"))
56+
}
57+
58+
@Test
59+
fun `an invalid custom scalar as input will throw an error`() {
60+
assertFails {
61+
graphQL.execute("{ uuidInput(uuid: \"foo\") }")
62+
}
63+
}
64+
65+
@Test
66+
fun `a custom scalar can be used as input in a list`() {
67+
val result = graphQL.execute("{ uuidListInput(uuids: [\"435f5808-936b-40ab-89b6-ec4e8cd1ba36\", \"435f5808-936b-40ab-89b6-ec4e8cd1ba36\"]) }")
68+
val data: Map<String, String>? = result.getData()
69+
70+
assertEquals("You sent 2 items and there are 1 unqiue items", data?.get("uuidListInput"))
71+
}
72+
73+
class QueryObject {
74+
fun uuidOutput(): UUID = UUID.randomUUID()
75+
76+
fun uuidInput(uuid: UUID) = "You sent $uuid"
77+
78+
fun uuidListInput(uuids: List<UUID>): String {
79+
// This verifies that the custom scalar is converted properly by jackson
80+
// and we can run comparisons on the original class
81+
val group = uuids.groupBy { it }
82+
return "You sent ${uuids.size} items and there are ${group.size} unqiue items"
83+
}
84+
}
85+
86+
class CustomScalarHooks : SchemaGeneratorHooks {
87+
override fun willGenerateGraphQLType(type: KType): GraphQLType? {
88+
return when (type.classifier as? KClass<*>) {
89+
UUID::class -> graphqlUUIDType
90+
else -> null
91+
}
92+
}
93+
}
94+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.test.utils
18+
19+
import graphql.language.StringValue
20+
import graphql.schema.Coercing
21+
import graphql.schema.GraphQLScalarType
22+
import java.util.UUID
23+
24+
/**
25+
* Custom scalar to parse a string as a [UUID] for both input and output.
26+
*/
27+
internal val graphqlUUIDType = GraphQLScalarType.newScalar()
28+
.name("UUID")
29+
.description("A type representing a formatted java.util.UUID")
30+
.coercing(UUIDCoercing)
31+
.build()
32+
33+
private object UUIDCoercing : Coercing<UUID, String> {
34+
override fun parseValue(input: Any?): UUID = UUID.fromString(serialize(input))
35+
36+
override fun parseLiteral(input: Any?): UUID? {
37+
val uuidString: String? = (input as? StringValue)?.value
38+
return if (uuidString != null) {
39+
UUID.fromString(uuidString)
40+
} else null
41+
}
42+
43+
override fun serialize(dataFetcherResult: Any?): String = dataFetcherResult.toString()
44+
}

0 commit comments

Comments
 (0)