diff --git a/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/ScalarQuery.kt b/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/ScalarQuery.kt index 3b95bcda94..effcbae651 100644 --- a/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/ScalarQuery.kt +++ b/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/ScalarQuery.kt @@ -32,6 +32,9 @@ class ScalarQuery : Query { @GraphQLDescription("generates random UUID") fun generateRandomUUID() = UUID.randomUUID() + @GraphQLDescription("Prints a string with a custom scalar as input") + fun printUuids(uuids: List) = "You sent $uuids" + fun findPersonById(@GraphQLID id: String) = Person(id, "Nelson") } diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcher.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcher.kt index 07a14384e5..7b46974c60 100644 --- a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcher.kt +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcher.kt @@ -16,10 +16,12 @@ package com.expediagroup.graphql.execution +import com.expediagroup.graphql.generator.extensions.getJavaClass import com.expediagroup.graphql.generator.extensions.getName +import com.expediagroup.graphql.generator.extensions.getTypeOfFirstArgument import com.expediagroup.graphql.generator.extensions.isDataFetchingEnvironment import com.expediagroup.graphql.generator.extensions.isGraphQLContext -import com.expediagroup.graphql.generator.extensions.javaTypeClass +import com.expediagroup.graphql.generator.extensions.isList import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import graphql.schema.DataFetcher @@ -95,14 +97,20 @@ open class FunctionDataFetcher( /** * Called to convert the generic input object to the parameter class. * - * This is currently achieved by using a Jackson [ObjectMapper]. + * This is currently achieved by using a Jackson ObjectMapper. */ protected open fun convertParameterValue(param: KParameter, environment: DataFetchingEnvironment): Any? { val name = param.getName() - val klazz = param.javaTypeClass() val argument = environment.arguments[name] - return objectMapper.convertValue(argument, klazz) + return if (param.isList()) { + val argumentClass = param.type.getTypeOfFirstArgument().getJavaClass() + val jacksonCollectionType = objectMapper.typeFactory.constructCollectionType(List::class.java, argumentClass) + objectMapper.convertValue(argument, jacksonCollectionType) + } else { + val javaClass = param.type.getJavaClass() + objectMapper.convertValue(argument, javaClass) + } } /** diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kParameterExtensions.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kParameterExtensions.kt index f891415bf4..609fa5cd2c 100644 --- a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kParameterExtensions.kt +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kParameterExtensions.kt @@ -22,7 +22,6 @@ import graphql.schema.DataFetchingEnvironment import kotlin.reflect.KParameter import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.isSubclassOf -import kotlin.reflect.jvm.javaType internal fun KParameter.isInterface() = this.type.getKClass().isInterface() @@ -35,10 +34,3 @@ internal fun KParameter.isDataFetchingEnvironment() = this.type.classifier == Da @Throws(CouldNotGetNameOfKParameterException::class) internal fun KParameter.getName(): String = this.getGraphQLName() ?: this.name ?: throw CouldNotGetNameOfKParameterException(this) - -internal fun KParameter.javaTypeClass(): Class<*> = - if (this.isList()) { - this.type.getKClass().java - } else { - this.type.javaType as Class<*> - } diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kTypeExtensions.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kTypeExtensions.kt index 09cae1d65b..5f24c9ef8a 100644 --- a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kTypeExtensions.kt +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kTypeExtensions.kt @@ -21,6 +21,7 @@ import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.full.createType import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.jvm.javaType import kotlin.reflect.jvm.jvmErasure private val primitiveArrayTypes = mapOf( @@ -35,6 +36,8 @@ private val primitiveArrayTypes = mapOf( internal fun KType.getKClass() = this.jvmErasure +internal fun KType.getJavaClass() = this.javaType as Class<*> + internal fun KType.isSubclassOf(kClass: KClass<*>) = this.getKClass().isSubclassOf(kClass) @Throws(InvalidListTypeException::class) diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/extensions/KParameterExtensionsKtTest.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/extensions/KParameterExtensionsKtTest.kt index a5c435bae7..8d65472eb5 100644 --- a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/extensions/KParameterExtensionsKtTest.kt +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/extensions/KParameterExtensionsKtTest.kt @@ -125,13 +125,6 @@ internal class KParameterExtensionsKtTest { assertFalse(param?.isDataFetchingEnvironment().isTrue()) } - @Test - fun javaTypeClass() { - assertEquals(expected = String::class.java, actual = MyKotlinClass::stringFun.findParameterByName("string")?.javaTypeClass()) - assertEquals(expected = List::class.java, actual = Container::listInput.findParameterByName("myList")?.javaTypeClass()) - assertEquals(expected = MyInterface::class.java, actual = Container::interfaceInput.findParameterByName("myInterface")?.javaTypeClass()) - } - @Test fun isList() { assertTrue(Container::listInput.findParameterByName("myList")?.isList() == true) diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooksTest.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooksTest.kt index 92f1d55f1d..c5226d896f 100644 --- a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooksTest.kt +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooksTest.kt @@ -23,10 +23,9 @@ import com.expediagroup.graphql.exceptions.EmptyInterfaceTypeException import com.expediagroup.graphql.exceptions.EmptyObjectTypeException import com.expediagroup.graphql.generator.extensions.getSimpleName import com.expediagroup.graphql.getTestSchemaConfigWithHooks +import com.expediagroup.graphql.test.utils.graphqlUUIDType import com.expediagroup.graphql.testSchemaConfig import com.expediagroup.graphql.toSchema -import graphql.language.StringValue -import graphql.schema.Coercing import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLInterfaceType import graphql.schema.GraphQLObjectType @@ -328,21 +327,4 @@ class SchemaGeneratorHooksTest { } class EmptyImplementation(override val id: String) : EmptyInterface - - private val graphqlUUIDType = GraphQLScalarType.newScalar() - .name("UUID") - .description("A type representing a formatted java.util.UUID") - .coercing(UUIDCoercing) - .build() - - private object UUIDCoercing : Coercing { - override fun parseValue(input: Any?): UUID = UUID.fromString(serialize(input)) - - override fun parseLiteral(input: Any?): UUID? { - val uuidString = (input as? StringValue)?.value - return UUID.fromString(uuidString) - } - - override fun serialize(dataFetcherResult: Any?): String = dataFetcherResult.toString() - } } diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/test/integration/CustomScalarExecutionTest.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/test/integration/CustomScalarExecutionTest.kt new file mode 100644 index 0000000000..8da31e88bf --- /dev/null +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/test/integration/CustomScalarExecutionTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2020 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.graphql.test.integration + +import com.expediagroup.graphql.TopLevelObject +import com.expediagroup.graphql.getTestSchemaConfigWithHooks +import com.expediagroup.graphql.hooks.SchemaGeneratorHooks +import com.expediagroup.graphql.test.utils.graphqlUUIDType +import com.expediagroup.graphql.toSchema +import graphql.GraphQL +import graphql.schema.GraphQLType +import org.junit.jupiter.api.Test +import java.util.UUID +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertNotNull + +class CustomScalarExecutionTest { + + private val schema = toSchema( + queries = listOf(TopLevelObject(QueryObject())), + config = getTestSchemaConfigWithHooks(CustomScalarHooks()) + ) + private val graphQL: GraphQL = GraphQL.newGraphQL(schema).build() + + @Test + fun `a custom scalar can be used as output`() { + val result = graphQL.execute("{ uuidOutput }") + val data: Map? = result.getData() + + assertNotNull(data?.get("uuidOutput")) + } + + @Test + fun `a custom scalar can be used as input`() { + val result = graphQL.execute("{ uuidInput(uuid: \"22dba784-f7c1-471f-8e8c-84e3a59bb5b5\") }") + val data: Map? = result.getData() + + assertNotNull(data?.get("uuidInput")) + } + + @Test + fun `an invalid custom scalar as input will throw an error`() { + assertFails { + graphQL.execute("{ uuidInput(uuid: \"foo\") }") + } + } + + @Test + fun `a custom scalar can be used as input in a list`() { + val result = graphQL.execute("{ uuidListInput(uuids: [\"435f5808-936b-40ab-89b6-ec4e8cd1ba36\", \"435f5808-936b-40ab-89b6-ec4e8cd1ba36\"]) }") + val data: Map? = result.getData() + + assertEquals("You sent 2 items and there are 1 unqiue items", data?.get("uuidListInput")) + } + + class QueryObject { + fun uuidOutput(): UUID = UUID.randomUUID() + + fun uuidInput(uuid: UUID) = "You sent $uuid" + + fun uuidListInput(uuids: List): String { + // This verifies that the custom scalar is converted properly by jackson + // and we can run comparisons on the original class + val group = uuids.groupBy { it } + return "You sent ${uuids.size} items and there are ${group.size} unqiue items" + } + } + + class CustomScalarHooks : SchemaGeneratorHooks { + override fun willGenerateGraphQLType(type: KType): GraphQLType? { + return when (type.classifier as? KClass<*>) { + UUID::class -> graphqlUUIDType + else -> null + } + } + } +} diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/test/utils/graphqlUuidType.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/test/utils/graphqlUuidType.kt new file mode 100644 index 0000000000..9d04c7f2fd --- /dev/null +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/test/utils/graphqlUuidType.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.graphql.test.utils + +import graphql.language.StringValue +import graphql.schema.Coercing +import graphql.schema.GraphQLScalarType +import java.util.UUID + +/** + * Custom scalar to parse a string as a [UUID] for both input and output. + */ +internal val graphqlUUIDType = GraphQLScalarType.newScalar() + .name("UUID") + .description("A type representing a formatted java.util.UUID") + .coercing(UUIDCoercing) + .build() + +private object UUIDCoercing : Coercing { + override fun parseValue(input: Any?): UUID = UUID.fromString(serialize(input)) + + override fun parseLiteral(input: Any?): UUID? { + val uuidString: String? = (input as? StringValue)?.value + return if (uuidString != null) { + UUID.fromString(uuidString) + } else null + } + + override fun serialize(dataFetcherResult: Any?): String = dataFetcherResult.toString() +}