diff --git a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/Application.kt b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/Application.kt index a59207c983..a4f4948e37 100644 --- a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/Application.kt +++ b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/Application.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import com.expediagroup.graphql.examples.server.spring.execution.SpringDataFetch import com.expediagroup.graphql.examples.server.spring.hooks.CustomSchemaGeneratorHooks import com.expediagroup.graphql.generator.directives.KotlinDirectiveWiringFactory import com.expediagroup.graphql.server.spring.subscriptions.ApolloSubscriptionHooks -import com.fasterxml.jackson.databind.ObjectMapper import graphql.execution.DataFetcherExceptionHandler import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @@ -43,9 +42,8 @@ class Application { @Bean fun dataFetcherFactoryProvider( springDataFetcherFactory: SpringDataFetcherFactory, - objectMapper: ObjectMapper, applicationContext: ApplicationContext - ) = CustomDataFetcherFactoryProvider(springDataFetcherFactory, objectMapper, applicationContext) + ) = CustomDataFetcherFactoryProvider(springDataFetcherFactory, applicationContext) @Bean fun dataFetcherExceptionHandler(): DataFetcherExceptionHandler = CustomDataFetcherExceptionHandler() diff --git a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/execution/CustomDataFetcherFactoryProvider.kt b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/execution/CustomDataFetcherFactoryProvider.kt index d38bdeb8a4..d98b91a516 100644 --- a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/execution/CustomDataFetcherFactoryProvider.kt +++ b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/execution/CustomDataFetcherFactoryProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package com.expediagroup.graphql.examples.server.spring.execution import com.expediagroup.graphql.generator.execution.SimpleKotlinDataFetcherFactoryProvider -import com.fasterxml.jackson.databind.ObjectMapper import graphql.schema.DataFetcherFactory import org.springframework.context.ApplicationContext import kotlin.reflect.KClass @@ -29,15 +28,13 @@ import kotlin.reflect.KProperty */ class CustomDataFetcherFactoryProvider( private val springDataFetcherFactory: SpringDataFetcherFactory, - private val objectMapper: ObjectMapper, private val applicationContext: ApplicationContext -) : SimpleKotlinDataFetcherFactoryProvider(objectMapper) { +) : SimpleKotlinDataFetcherFactoryProvider() { override fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>) = DataFetcherFactory { CustomFunctionDataFetcher( target = target, fn = kFunction, - objectMapper = objectMapper, appContext = applicationContext ) } diff --git a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/execution/CustomFunctionDataFetcher.kt b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/execution/CustomFunctionDataFetcher.kt index 9efd6108af..31f10f7722 100644 --- a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/execution/CustomFunctionDataFetcher.kt +++ b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/execution/CustomFunctionDataFetcher.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package com.expediagroup.graphql.examples.server.spring.execution import com.expediagroup.graphql.server.spring.execution.SpringDataFetcher -import com.fasterxml.jackson.databind.ObjectMapper import graphql.schema.DataFetchingEnvironment import org.springframework.context.ApplicationContext import reactor.core.publisher.Mono @@ -29,9 +28,8 @@ import kotlin.reflect.KFunction class CustomFunctionDataFetcher( target: Any?, fn: KFunction<*>, - objectMapper: ObjectMapper, appContext: ApplicationContext -) : SpringDataFetcher(target, fn, objectMapper, appContext) { +) : SpringDataFetcher(target, fn, appContext) { override fun get(environment: DataFetchingEnvironment): Any? = when (val result = super.get(environment)) { is Mono<*> -> result.toFuture() diff --git a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/hooks/CustomSchemaGeneratorHooks.kt b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/hooks/CustomSchemaGeneratorHooks.kt index 696d18334c..1162ec367f 100644 --- a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/hooks/CustomSchemaGeneratorHooks.kt +++ b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/hooks/CustomSchemaGeneratorHooks.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLType import org.springframework.beans.factory.BeanFactoryAware import reactor.core.publisher.Mono +import java.time.LocalDate import java.util.UUID import kotlin.reflect.KClass import kotlin.reflect.KType @@ -43,6 +44,12 @@ class CustomSchemaGeneratorHooks(override val wiringFactory: KotlinDirectiveWiri */ override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier) { UUID::class -> graphqlUUIDType + ClosedRange::class -> { + when (type.arguments[0].type?.classifier as? KClass<*>) { + LocalDate::class -> graphqlPeriodType + else -> null + } + } else -> null } @@ -94,3 +101,36 @@ private object UUIDCoercing : Coercing { throw CoercingSerializeException("Data fetcher result $dataFetcherResult cannot be serialized to a String") } } + +internal val graphqlPeriodType: GraphQLScalarType = GraphQLScalarType.newScalar() + .name("Period") + .description("""A period of local date to local date, inclusive on both ends i.e. a closed range.""") + .coercing(PeriodCoercing) + .build() + +typealias Period = ClosedRange + +private object PeriodCoercing : Coercing { + override fun parseValue(input: Any): Period = runCatching { + input.toString().parseAsPeriod() + }.getOrElse { + throw CoercingParseValueException("Expected valid Period but was $input") + } + + override fun parseLiteral(input: Any): Period = runCatching { + (input as? StringValue)?.value?.parseAsPeriod() ?: throw CoercingParseLiteralException("Expected valid Period literal but was $input") + }.getOrElse { + throw CoercingParseLiteralException("Expected valid Period literal but was $input") + } + + override fun serialize(dataFetcherResult: Any): String = kotlin.runCatching { + toString() + }.getOrElse { + throw CoercingSerializeException("Data fetcher result $dataFetcherResult cannot be serialized to a String") + } + + private fun String.parseAsPeriod(): Period = split("..").let { + if (it.size != 2) error("Cannot parse input $this as Period") + LocalDate.parse(it[0])..LocalDate.parse(it[1]) + } +} diff --git a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/model/Widget.kt b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/model/Widget.kt index cd1b7b4ecd..0e0ee7b51e 100644 --- a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/model/Widget.kt +++ b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/model/Widget.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,8 @@ data class Widget( @GraphQLDescription("The widget's deprecated value that shouldn't be used") val deprecatedValue: Int? = value, + val listOfValues: List? = null, + @GraphQLIgnore val ignoredField: String? = "ignored", diff --git a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/mutation/WidgetMutation.kt b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/mutation/WidgetMutation.kt index 3e38c17e84..fc9d21ef95 100644 --- a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/mutation/WidgetMutation.kt +++ b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/mutation/WidgetMutation.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,4 +34,14 @@ class WidgetMutation : Mutation { } return widget } + + fun processWidgetList(widgets: List): List { + widgets.forEach { + if (null == it.value) { + it.value = 42 + } + } + + return widgets + } } diff --git a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/query/ScalarQuery.kt b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/query/ScalarQuery.kt index bcaa044c2f..4554429ba2 100644 --- a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/query/ScalarQuery.kt +++ b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/query/ScalarQuery.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.expediagroup.graphql.examples.server.spring.query +import com.expediagroup.graphql.examples.server.spring.hooks.Period import com.expediagroup.graphql.examples.server.spring.model.Person import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.generator.scalars.ID @@ -32,6 +33,9 @@ class ScalarQuery : Query { @GraphQLDescription("generates random UUID") fun generateRandomUUID() = UUID.randomUUID() + @GraphQLDescription("Prints a string with a custom scalar as input") + fun printUuid(uuid: UUID) = "You sent $uuid" + @GraphQLDescription("Prints a string with a custom scalar as input") fun printUuids(uuids: List) = "You sent $uuids" @@ -39,4 +43,11 @@ class ScalarQuery : Query { @GraphQLDescription("generates random GraphQL ID") fun generateRandomId() = ID(UUID.randomUUID().toString()) + + fun customScalarInput(input: CustomScalarInput): String = "foo is ${input.foo} and range is ${input.range}" + + data class CustomScalarInput( + val foo: String, + val range: Period, + ) } diff --git a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/query/SimpleQuery.kt b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/query/SimpleQuery.kt index 51d066888a..27b2857f0f 100644 --- a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/query/SimpleQuery.kt +++ b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/query/SimpleQuery.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,15 +62,6 @@ class SimpleQuery : Query { return (1..10).map { random.nextInt(100) }.toList() } - @GraphQLDescription("generates pseudo random array of ints") - fun generatePrimitiveArray(): IntArray { - val random = Random() - return (1..10).map { random.nextInt(100) }.toIntArray() - } - - @GraphQLDescription("query with array input") - fun doSomethingWithIntArray(ints: IntArray) = "received ints=[${ints.joinToString()}]" - @GraphQLDescription("query with optional input") fun doSomethingWithOptionalInput( @GraphQLDescription("this field is required") requiredValue: Int, diff --git a/examples/server/spring-server/src/test/kotlin/com/expediagroup/graphql/examples/server/spring/mutation/ScalarMutationIT.kt b/examples/server/spring-server/src/test/kotlin/com/expediagroup/graphql/examples/server/spring/mutation/ScalarMutationIT.kt index dbed9f1327..e1b27687d9 100644 --- a/examples/server/spring-server/src/test/kotlin/com/expediagroup/graphql/examples/server/spring/mutation/ScalarMutationIT.kt +++ b/examples/server/spring-server/src/test/kotlin/com/expediagroup/graphql/examples/server/spring/mutation/ScalarMutationIT.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ class ScalarMutationIT(@Autowired private val testClient: WebTestClient) { .uri(GRAPHQL_ENDPOINT) .accept(APPLICATION_JSON) .contentType(GRAPHQL_MEDIA_TYPE) - .bodyValue("mutation { $query(person: {id: 1, name: \"Alice\"}) { id, name } }") + .bodyValue("mutation { $query(person: {id: \"1\", name: \"Alice\"}) { id, name } }") .exchange() .verifyOnlyDataExists(query) .jsonPath("$DATA_JSON_PATH.$query.id").isEqualTo(1) diff --git a/examples/server/spring-server/src/test/kotlin/com/expediagroup/graphql/examples/server/spring/query/SimpleQueryIT.kt b/examples/server/spring-server/src/test/kotlin/com/expediagroup/graphql/examples/server/spring/query/SimpleQueryIT.kt index 72974a309f..4d8d85bf5c 100644 --- a/examples/server/spring-server/src/test/kotlin/com/expediagroup/graphql/examples/server/spring/query/SimpleQueryIT.kt +++ b/examples/server/spring-server/src/test/kotlin/com/expediagroup/graphql/examples/server/spring/query/SimpleQueryIT.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -162,36 +162,6 @@ class SimpleQueryIT(@Autowired private val testClient: WebTestClient) { .jsonPath("$DATA_JSON_PATH.$query").value(hasSize(10)) } - @Test - fun `verify generatePrimitiveArray query`() { - val query = "generatePrimitiveArray" - - testClient.post() - .uri(GRAPHQL_ENDPOINT) - .accept(APPLICATION_JSON) - .contentType(GRAPHQL_MEDIA_TYPE) - .bodyValue("query { $query }") - .exchange() - .expectStatus().isOk - .verifyOnlyDataExists(query) - .jsonPath("$DATA_JSON_PATH.$query").isArray - .jsonPath("$DATA_JSON_PATH.$query").value(hasSize(10)) - } - - @Test - fun `verify doSomethingWithIntArray query`() { - val query = "doSomethingWithIntArray" - val expectedData = "received ints=[1, 2, 3, 4, 5]" - - testClient.post() - .uri(GRAPHQL_ENDPOINT) - .accept(APPLICATION_JSON) - .contentType(GRAPHQL_MEDIA_TYPE) - .bodyValue("query { $query(ints: [1, 2, 3, 4, 5]) }") - .exchange() - .verifyData(query, expectedData) - } - @Test fun `verify doSomethingWithOptionalInput query`() { val query = "doSomethingWithOptionalInput" diff --git a/generator/graphql-kotlin-schema-generator/build.gradle.kts b/generator/graphql-kotlin-schema-generator/build.gradle.kts index 359ac2fd6c..69030e7c95 100644 --- a/generator/graphql-kotlin-schema-generator/build.gradle.kts +++ b/generator/graphql-kotlin-schema-generator/build.gradle.kts @@ -2,7 +2,6 @@ description = "Code-only GraphQL schema generation for Kotlin" val classGraphVersion: String by project val graphQLJavaVersion: String by project -val jacksonVersion: String by project val kotlinCoroutinesVersion: String by project val rxjavaVersion: String by project val junitVersion: String by project @@ -11,7 +10,6 @@ val slf4jVersion: String by project dependencies { api("com.graphql-java:graphql-java:$graphQLJavaVersion") api("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$kotlinCoroutinesVersion") - api("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") implementation("io.github.classgraph:classgraph:$classGraphVersion") implementation("org.slf4j:slf4j-api:$slf4jVersion") testImplementation("io.reactivex.rxjava3:rxjava:$rxjavaVersion") diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/exceptions/PrimaryConstructorNotFound.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/exceptions/PrimaryConstructorNotFound.kt new file mode 100644 index 0000000000..758bf403ca --- /dev/null +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/exceptions/PrimaryConstructorNotFound.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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.generator.exceptions + +import kotlin.reflect.KClass + +/** + * Thrown when unable to locate the public primary constructor of an input class. + */ +class PrimaryConstructorNotFound(klazz: KClass<*>) : GraphQLKotlinException("Invalid input object ${klazz.simpleName} - missing public primary constructor") diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/FunctionDataFetcher.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/FunctionDataFetcher.kt index f01e84cb00..6472d0a523 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/FunctionDataFetcher.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/FunctionDataFetcher.kt @@ -17,17 +17,10 @@ package com.expediagroup.graphql.generator.execution import com.expediagroup.graphql.generator.extensions.getOrDefault -import com.expediagroup.graphql.generator.internal.extensions.getJavaClass import com.expediagroup.graphql.generator.internal.extensions.getName -import com.expediagroup.graphql.generator.internal.extensions.getTypeOfFirstArgument -import com.expediagroup.graphql.generator.internal.extensions.getWrappedType -import com.expediagroup.graphql.generator.internal.extensions.isArray import com.expediagroup.graphql.generator.internal.extensions.isDataFetchingEnvironment import com.expediagroup.graphql.generator.internal.extensions.isGraphQLContext -import com.expediagroup.graphql.generator.internal.extensions.isList import com.expediagroup.graphql.generator.internal.extensions.isOptionalInputType -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.CoroutineScope @@ -37,7 +30,6 @@ import java.util.concurrent.CompletableFuture import kotlin.coroutines.EmptyCoroutineContext import kotlin.reflect.KFunction import kotlin.reflect.KParameter -import kotlin.reflect.KType import kotlin.reflect.full.callSuspendBy import kotlin.reflect.full.instanceParameter import kotlin.reflect.full.valueParameters @@ -48,13 +40,11 @@ import kotlin.reflect.full.valueParameters * @param target The target object that performs the data fetching, if not specified then this data fetcher will attempt * to use source object from the environment * @param fn The Kotlin function being invoked - * @param objectMapper Jackson ObjectMapper that will be used to deserialize environment arguments to the expected function arguments */ @Suppress("Detekt.SpreadOperator") open class FunctionDataFetcher( private val target: Any?, private val fn: KFunction<*>, - private val objectMapper: ObjectMapper = jacksonObjectMapper() ) : DataFetcher { /** @@ -108,58 +98,13 @@ open class FunctionDataFetcher( else -> { val name = param.getName() if (environment.containsArgument(name) || param.type.isOptionalInputType()) { - val value: Any? = environment.arguments[name] - param to convertArgumentToObject(param, environment, name, value) + param to convertArgumentValue(name, param, environment.arguments) } else { null } } } - /** - * Convert the generic argument value from JSON input to the parameter class. - * This is currently achieved by using a Jackson ObjectMapper. - */ - private fun convertArgumentToObject( - param: KParameter, - environment: DataFetchingEnvironment, - argumentName: String, - argumentValue: Any? - ): Any? = when { - param.type.isOptionalInputType() -> { - when { - !environment.containsArgument(argumentName) -> OptionalInput.Undefined - argumentValue == null -> OptionalInput.Defined(null) - else -> { - val paramType = param.type.getTypeOfFirstArgument() - val value = convertValue(paramType, argumentValue) - OptionalInput.Defined(value) - } - } - } - else -> convertValue(param.type, argumentValue) - } - - private fun convertValue( - paramType: KType, - argumentValue: Any? - ): Any? = when { - paramType.isList() -> { - val argumentClass = paramType.getTypeOfFirstArgument().getJavaClass() - val jacksonCollectionType = objectMapper.typeFactory.constructCollectionType(List::class.java, argumentClass) - objectMapper.convertValue(argumentValue, jacksonCollectionType) - } - paramType.isArray() -> { - val argumentClass = paramType.getWrappedType().getJavaClass() - val jacksonCollectionType = objectMapper.typeFactory.constructArrayType(argumentClass) - objectMapper.convertValue(argumentValue, jacksonCollectionType) - } - else -> { - val javaClass = paramType.getJavaClass() - objectMapper.convertValue(argumentValue, javaClass) - } - } - /** * Once all parameters values are properly converted, this function will be called to run a suspendable function using * a scope provided in the GraphQLContext map or default to a new CoroutineScope with EmptyCoroutineContext. diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/KotlinDataFetcherFactoryProvider.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/KotlinDataFetcherFactoryProvider.kt index 59a3d37e29..b9912e0e1a 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/KotlinDataFetcherFactoryProvider.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/KotlinDataFetcherFactoryProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package com.expediagroup.graphql.generator.execution -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import graphql.schema.DataFetcherFactory import kotlin.reflect.KClass import kotlin.reflect.KFunction @@ -51,15 +49,12 @@ interface KotlinDataFetcherFactoryProvider { * SimpleKotlinDataFetcherFactoryProvider is the default data fetcher factory provider that is used during schema construction * to obtain [DataFetcherFactory] that should be used for target function and property resolution. */ -open class SimpleKotlinDataFetcherFactoryProvider( - private val objectMapper: ObjectMapper = jacksonObjectMapper() -) : KotlinDataFetcherFactoryProvider { +open class SimpleKotlinDataFetcherFactoryProvider : KotlinDataFetcherFactoryProvider { override fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>) = DataFetcherFactory { FunctionDataFetcher( target = target, - fn = kFunction, - objectMapper = objectMapper + fn = kFunction ) } diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/OptionalInput.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/OptionalInput.kt index 8e6b126147..8db90eaf36 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/OptionalInput.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/OptionalInput.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,10 @@ package com.expediagroup.graphql.generator.execution -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonValue -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.BeanProperty -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.deser.ContextualDeserializer -import com.fasterxml.jackson.databind.util.AccessPattern - /** * Wrapper used to represent optionally defined input arguments that allows us to distinguish between undefined value, explicit NULL value * and specified value. */ -@JsonDeserialize(using = OptionalInputDeserializer::class) sealed class OptionalInput { /** @@ -43,36 +32,7 @@ sealed class OptionalInput { /** * Wrapper holding explicitly specified value including NULL. */ - class Defined @JsonCreator constructor(@JsonValue val value: U?) : OptionalInput() { + class Defined(val value: U?) : OptionalInput() { override fun toString(): String = "Defined(value=$value)" } } - -/** - * Null aware deserializer that distinguishes between undefined value, explicit NULL value - * and specified value. - */ -class OptionalInputDeserializer(private val klazz: Class<*>? = null) : JsonDeserializer>(), ContextualDeserializer { - - override fun createContextual(ctxt: DeserializationContext, property: BeanProperty?): JsonDeserializer<*> { - val type = if (property != null) { - property.type.containedType(0) - } else { - ctxt.contextualType - } - return OptionalInputDeserializer(type.rawClass) - } - - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): OptionalInput<*> { - val result: Any = ctxt.readValue(p, klazz) - return OptionalInput.Defined(result) - } - - override fun getNullAccessPattern(): AccessPattern = AccessPattern.CONSTANT - - override fun getNullValue(ctxt: DeserializationContext): OptionalInput<*> = if (ctxt.parser.parsingContext.currentName != null) { - OptionalInput.Defined(null) - } else { - OptionalInput.Undefined - } -} diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/convertArgumentValue.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/convertArgumentValue.kt new file mode 100644 index 0000000000..05b7d14bbc --- /dev/null +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/convertArgumentValue.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2022 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.generator.execution + +import com.expediagroup.graphql.generator.exceptions.PrimaryConstructorNotFound +import com.expediagroup.graphql.generator.internal.extensions.getKClass +import com.expediagroup.graphql.generator.internal.extensions.getName +import com.expediagroup.graphql.generator.internal.extensions.getTypeOfFirstArgument +import com.expediagroup.graphql.generator.internal.extensions.isOptionalInputType +import com.expediagroup.graphql.generator.internal.extensions.isSubclassOf +import kotlin.reflect.KClass +import kotlin.reflect.KParameter +import kotlin.reflect.KType +import kotlin.reflect.full.primaryConstructor + +/** + * Convert the argument from the argument map to a class we can pass to the Kotlin function. + */ +internal fun convertArgumentValue( + argumentName: String, + param: KParameter, + argumentMap: Map +): Any? { + val argumentValue = argumentMap[argumentName] + return when { + param.type.isOptionalInputType() -> { + when { + !argumentMap.containsKey(argumentName) -> OptionalInput.Undefined + argumentValue == null -> OptionalInput.Defined(null) + else -> { + val paramType = param.type.getTypeOfFirstArgument() + val value = convertValue(paramType, argumentValue) + OptionalInput.Defined(value) + } + } + } + else -> convertValue(param.type, argumentValue) + } +} + +/** + * The value may already be parsed, so we can perform some checks to avoid duplicate deserialization + */ +private fun convertValue( + paramType: KType, + argumentValue: Any? +): Any? { + // The input given is a list, iterate over each value to return a parsed list + if (argumentValue is Iterable<*>) { + return argumentValue.map { + val wrappedType = paramType.getTypeOfFirstArgument() + convertValue(wrappedType, it) + } + } + + // If the value is a generic map, parse each entry which may have some values already parsed + if (argumentValue is Map<*, *>) { + @Suppress("UNCHECKED_CAST") + return mapToKotlinObject(argumentValue as Map, paramType.getKClass()) + } + + // If the value is enum we need to find the correct value + if (argumentValue is String && paramType.isSubclassOf(Enum::class)) { + return mapToEnumValue(paramType, argumentValue) + } + + // If param type is inline value class we need to wrap the value + if (argumentValue != null && paramType.getKClass().isValue) { + return mapToInlineValueClass(argumentValue, paramType.getKClass()) + } + + // Value is already parsed so we can return it as-is + return argumentValue +} + +/** + * At this point all custom scalars have been converted by graphql-java so the only thing left to parse is object maps into the nested Kotlin classes + */ +private fun mapToKotlinObject(inputMap: Map, targetClass: KClass): T { + val targetConstructor = targetClass.primaryConstructor ?: throw PrimaryConstructorNotFound(targetClass) + val params = targetConstructor.parameters + val constructorValues: Map = params.associateWith { parameter -> + convertArgumentValue(parameter.getName(), parameter, inputMap) + } + return targetConstructor.callBy(constructorValues) +} + +private fun mapToEnumValue(paramType: KType, enumValue: String): Enum<*> = + paramType.getKClass().java.enumConstants.filterIsInstance(Enum::class.java).first { it.name == enumValue } + +private fun mapToInlineValueClass(value: Any?, targetClass: KClass): T { + val targetConstructor = targetClass.primaryConstructor ?: throw PrimaryConstructorNotFound(targetClass) + return targetConstructor.call(value) +} diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/extensions/kClassExtensions.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/extensions/kClassExtensions.kt index 6b56b75b84..73db567708 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/extensions/kClassExtensions.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/extensions/kClassExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,7 +77,7 @@ internal fun KClass<*>.isValidAdditionalType(inputType: Boolean): Boolean = !(in internal fun KClass<*>.isEnum(): Boolean = this.isSubclassOf(Enum::class) -internal fun KClass<*>.isListType(): Boolean = this.isSubclassOf(List::class) || this.java.isArray +internal fun KClass<*>.isListType(): Boolean = this.isSubclassOf(List::class) @Throws(CouldNotGetNameOfKClassException::class) internal fun KClass<*>.getSimpleName(isInputClass: Boolean = false): String { diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/extensions/kTypeExtensions.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/extensions/kTypeExtensions.kt index 81863dafc2..d0b819d16a 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/extensions/kTypeExtensions.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/extensions/kTypeExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,21 +20,10 @@ import com.expediagroup.graphql.generator.exceptions.InvalidWrappedTypeException import com.expediagroup.graphql.generator.execution.OptionalInput import kotlin.reflect.KClass import kotlin.reflect.KType -import kotlin.reflect.full.createType import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.withNullability import kotlin.reflect.jvm.jvmErasure -private val primitiveArrayTypes = mapOf( - IntArray::class to Int::class, - LongArray::class to Long::class, - ShortArray::class to Short::class, - FloatArray::class to Float::class, - DoubleArray::class to Double::class, - CharArray::class to Char::class, - BooleanArray::class to Boolean::class -) - internal fun KType.getKClass() = this.jvmErasure internal fun KType.getJavaClass(): Class<*> = this.getKClass().java @@ -50,7 +39,7 @@ internal fun KType.isListType() = this.isList() || this.isArray() internal fun KType.isOptionalInputType() = this.isSubclassOf(OptionalInput::class) internal fun KType.unwrapOptionalInputType() = if (this.isOptionalInputType()) { - this.getWrappedType().withNullability(true) + this.getTypeOfFirstArgument().withNullability(true) } else { this } @@ -59,14 +48,6 @@ internal fun KType.unwrapOptionalInputType() = if (this.isOptionalInputType()) { internal fun KType.getTypeOfFirstArgument(): KType = this.arguments.firstOrNull()?.type ?: throw InvalidWrappedTypeException(this) -internal fun KType.getWrappedType(): KType { - val primitiveClass = primitiveArrayTypes[this.getKClass()] - return when { - primitiveClass != null -> primitiveClass.createType() - else -> this.getTypeOfFirstArgument() - } -} - internal fun KType.getSimpleName(isInputType: Boolean = false): String = this.getKClass().getSimpleName(isInputType) internal val KType.qualifiedName: String diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateArgument.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateArgument.kt index aaf95b7d78..45db0ac648 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateArgument.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateArgument.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import com.expediagroup.graphql.generator.exceptions.InvalidInputFieldTypeExcept import com.expediagroup.graphql.generator.internal.extensions.getGraphQLDescription import com.expediagroup.graphql.generator.internal.extensions.getKClass import com.expediagroup.graphql.generator.internal.extensions.getName -import com.expediagroup.graphql.generator.internal.extensions.getWrappedType +import com.expediagroup.graphql.generator.internal.extensions.getTypeOfFirstArgument import com.expediagroup.graphql.generator.internal.extensions.isInterface import com.expediagroup.graphql.generator.internal.extensions.isListType import com.expediagroup.graphql.generator.internal.extensions.isUnion @@ -65,7 +65,7 @@ internal fun generateArgument(generator: SchemaGenerator, parameter: KParameter) private fun getUnwrappedClass(parameterType: KType): KClass<*> = if (parameterType.isListType()) { - parameterType.getWrappedType().getKClass() + parameterType.getTypeOfFirstArgument().getKClass() } else { parameterType.getKClass() } diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateInputObject.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateInputObject.kt index e9adc7e74a..474a92d9b9 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateInputObject.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateInputObject.kt @@ -24,12 +24,14 @@ import com.expediagroup.graphql.generator.internal.extensions.getValidProperties import com.expediagroup.graphql.generator.internal.extensions.safeCast import com.expediagroup.graphql.generator.internal.types.utils.validateGraphQLName import com.expediagroup.graphql.generator.internal.types.utils.validateObjectLocation +import com.expediagroup.graphql.generator.internal.types.utils.validatePrimaryConstructorExists import graphql.introspection.Introspection.DirectiveLocation import graphql.schema.GraphQLInputObjectType import kotlin.reflect.KClass internal fun generateInputObject(generator: SchemaGenerator, kClass: KClass<*>): GraphQLInputObjectType { validateObjectLocation(kClass, GraphQLValidObjectLocations.Locations.INPUT_OBJECT) + validatePrimaryConstructorExists(kClass) val name = kClass.getSimpleName(isInputClass = true) validateGraphQLName(name, kClass) diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateList.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateList.kt index 2327451724..fa2cdf0721 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateList.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateList.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,11 @@ package com.expediagroup.graphql.generator.internal.types import com.expediagroup.graphql.generator.SchemaGenerator -import com.expediagroup.graphql.generator.internal.extensions.getWrappedType +import com.expediagroup.graphql.generator.internal.extensions.getTypeOfFirstArgument import graphql.schema.GraphQLList import kotlin.reflect.KType internal fun generateList(generator: SchemaGenerator, type: KType, typeInfo: GraphQLKTypeMetadata): GraphQLList { - val wrappedType = generateGraphQLType(generator, type.getWrappedType(), typeInfo) + val wrappedType = generateGraphQLType(generator, type.getTypeOfFirstArgument(), typeInfo) return GraphQLList.list(wrappedType) } diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/utils/validatePrimaryConstructorExists.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/utils/validatePrimaryConstructorExists.kt new file mode 100644 index 0000000000..c84ef62c84 --- /dev/null +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/utils/validatePrimaryConstructorExists.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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.generator.internal.types.utils + +import com.expediagroup.graphql.generator.exceptions.PrimaryConstructorNotFound +import com.expediagroup.graphql.generator.internal.extensions.isPublic +import kotlin.reflect.KClass +import kotlin.reflect.full.primaryConstructor + +/** + * Throws an exception if this KClass does not specify valid primary constructor. + */ +internal fun validatePrimaryConstructorExists(kClass: KClass<*>) { + if (kClass.primaryConstructor?.isPublic() == false) { + throw PrimaryConstructorNotFound(kClass) + } +} diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/scalars/ID.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/scalars/ID.kt index 608c80a4e9..efa991ac03 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/scalars/ID.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/scalars/ID.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,10 @@ package com.expediagroup.graphql.generator.scalars -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonValue - /** - * Used to represent a GraphQL ID scalar type - * which must serialize/deserialize to a string value + * Used to represent a GraphQL ID scalar type which must serialize/deserialize to a string value */ -data class ID(@get:JsonIgnore val value: String) { - @JsonValue - override fun toString() = value +@JvmInline +value class ID(val value: String) { + override fun toString(): String = value } diff --git a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/ToSchemaTest.kt b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/ToSchemaTest.kt index 2ec814331d..e51e798178 100644 --- a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/ToSchemaTest.kt +++ b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/ToSchemaTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,37 +86,6 @@ class ToSchemaTest { assertEquals(1, geo?.get("query")?.get("id")) } - @Test - fun `Schema generator exposes arrays and lists as function arguments`() { - val schema = toSchema(queries = listOf(TopLevelObject(QueryWithLists())), config = testSchemaConfig) - val firstArgumentType = schema.queryType.getFieldDefinition("sumOf").arguments[0].type.deepName - assertEquals("[Int!]!", firstArgumentType) - val secondArgumentType = schema.queryType.getFieldDefinition("sumOfList").arguments[0].type.deepName - assertEquals("[Int!]!", secondArgumentType) - - val graphQL = GraphQL.newGraphQL(schema).build() - val arrayResult = graphQL.execute("{ sumOf(ints: [1, 2, 3]) }") - val arraySum = arrayResult.getData>().values.first() - assertEquals(6, arraySum) - - val listResult = graphQL.execute("{ sumOfList(ints: [1, 2, 3]) }") - val listSum = listResult.getData>().values.first() - assertEquals(6, listSum) - } - - @Test - fun `Schema generator exposes arrays of complex types as function arguments`() { - val schema = toSchema(queries = listOf(TopLevelObject(QueryWithLists())), config = testSchemaConfig) - val firstArgumentType = schema.queryType.getFieldDefinition("sumOfComplexArray").arguments[0].type.deepName - assertEquals("[ComplexWrappingTypeInput!]!", firstArgumentType) - - val graphQL = GraphQL.newGraphQL(schema).build() - val result = graphQL.execute("{sumOfComplexArray(objects: [{value: 1}, {value: 2}, {value: 3}])}") - val sum = result.getData>().values.first() - - assertEquals(6, sum) - } - @Test fun `SchemaGenerator ignores fields and functions with @Ignore`() { val schema = toSchema(queries = listOf(TopLevelObject(QueryWithIgnored())), config = testSchemaConfig) @@ -364,12 +333,6 @@ class ToSchemaTest { fun query(@GraphQLDescription("A GraphQL value") value: Int): Geography = Geography(value, GeoType.CITY, listOf()) } - class QueryWithLists { - fun sumOf(ints: IntArray): Int = ints.sum() - fun sumOfComplexArray(objects: Array): Int = objects.map { it.value }.sum() - fun sumOfList(ints: List): Int = ints.sum() - } - class QueryWithIgnored { fun query(): ResultWithIgnored? = null diff --git a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/execution/ConvertArgumentValueTest.kt b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/execution/ConvertArgumentValueTest.kt new file mode 100644 index 0000000000..6033477971 --- /dev/null +++ b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/execution/ConvertArgumentValueTest.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2022 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.generator.execution + +import com.expediagroup.graphql.generator.scalars.ID +import graphql.schema.DataFetchingEnvironment +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test +import kotlin.reflect.full.findParameterByName +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class ConvertArgumentValueTest { + + @Test + fun `string input is parsed`() { + val kParam = assertNotNull(TestFunctions::stringInput.findParameterByName("input")) + val result = convertArgumentValue("input", kParam, mapOf("input" to "hello")) + assertEquals("hello", result) + } + + @Test + fun `pre-parsed object is returned`() { + val kParam = assertNotNull(TestFunctions::inputObject.findParameterByName("input")) + val result = convertArgumentValue("input", kParam, mapOf("input" to TestInput("hello"))) + val castResult = assertIs(result) + assertEquals("hello", castResult.foo) + } + + @Test + fun `enum object is parsed`() { + val kParam = assertNotNull(TestFunctions::enumInput.findParameterByName("input")) + val inputValue = "BAR" + val result = convertArgumentValue("input", kParam, mapOf("input" to inputValue)) + val castResult = assertIs(result) + assertEquals(Foo.BAR, castResult) + } + + @Test + fun `generic map object is parsed`() { + val kParam = assertNotNull(TestFunctions::inputObject.findParameterByName("input")) + val inputValue = mapOf( + "foo" to "hello", + "bar" to "world", + "baz" to listOf("!"), + "qux" to "1234" + ) + val result = convertArgumentValue("input", kParam, mapOf("input" to inputValue)) + val castResult = assertIs(result) + assertEquals("hello", castResult.foo) + assertEquals("world", castResult.bar) + assertEquals(listOf("!"), castResult.baz) + assertEquals("1234", castResult.qux?.value) + } + + @Test + fun `generic map object is parsed and defaults are used`() { + val kParam = assertNotNull(TestFunctions::inputObject.findParameterByName("input")) + val result = convertArgumentValue("input", kParam, mapOf("input" to mapOf("foo" to "hello"))) + val castResult = assertIs(result) + assertEquals("hello", castResult.foo) + assertEquals(null, castResult.bar) + assertEquals(null, castResult.baz) + assertEquals(null, castResult.qux) + } + + @Test + fun `list string input is parsed`() { + val kParam = assertNotNull(TestFunctions::listStringInput.findParameterByName("input")) + val result = convertArgumentValue("input", kParam, mapOf("input" to listOf("hello"))) + assertEquals(listOf("hello"), result) + } + + @Test + fun `optional input when undefined is parsed`() { + val kParam = assertNotNull(TestFunctions::optionalInput.findParameterByName("input")) + val result = convertArgumentValue("input", kParam, mapOf()) + assertEquals(OptionalInput.Undefined, result) + } + + @Test + fun `optional input with defined null is parsed`() { + val kParam = assertNotNull(TestFunctions::optionalInput.findParameterByName("input")) + val result = convertArgumentValue("input", kParam, mapOf("input" to null)) + val castResult = assertIs>(result) + assertEquals(null, castResult.value) + } + + @Test + fun `optional input with defined value is parsed`() { + val kParam = assertNotNull(TestFunctions::optionalInput.findParameterByName("input")) + val mockEnv = mockk { + every { containsArgument("input") } returns true + } + val result = convertArgumentValue("input", kParam, mapOf("input" to "hello")) + val castResult = assertIs>(result) + assertEquals("hello", castResult.value) + } + + @Test + fun `optional input with object is parsed`() { + val kParam = assertNotNull(TestFunctions::optionalInputObject.findParameterByName("input")) + val result = convertArgumentValue("input", kParam, mapOf("input" to TestInput("hello"))) + val castResult = assertIs>(result) + val castResult2 = assertIs(castResult.value) + assertEquals("hello", castResult2.foo) + } + + @Test + fun `optional input with list object is parsed`() { + val kParam = assertNotNull(TestFunctions::optionalInputListObject.findParameterByName("input")) + val result = convertArgumentValue("input", kParam, mapOf("input" to listOf(TestInput("hello")))) + val castResult = assertIs>(result) + val castResult2 = assertIs>(castResult.value) + assertEquals("hello", castResult2.firstOrNull()?.foo) + } + + @Test + fun `id input is parsed`() { + val kParam = assertNotNull(TestFunctions::idInput.findParameterByName("input")) + val result = convertArgumentValue("input", kParam, mapOf("input" to "1234")) + assertIs(result) + assertEquals("1234", result.value) + } + + class TestFunctions { + fun enumInput(input: Foo): String = TODO() + fun idInput(input: ID): String = TODO() + fun inputObject(input: TestInput): String = TODO() + fun listStringInput(input: List): String = TODO() + fun optionalInput(input: OptionalInput): String = TODO() + fun optionalInputObject(input: OptionalInput): String = TODO() + fun optionalInputListObject(input: OptionalInput>): String = TODO() + fun stringInput(input: String): String = TODO() + } + + class TestInput( + val foo: String, + val bar: String? = null, + val baz: List? = null, + val qux: ID? = null + ) + + enum class Foo { + BAR, + BAZ + } +} diff --git a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/execution/FunctionDataFetcherTest.kt b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/execution/FunctionDataFetcherTest.kt index ebe00824ad..1576173a6a 100644 --- a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/execution/FunctionDataFetcherTest.kt +++ b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/execution/FunctionDataFetcherTest.kt @@ -18,7 +18,6 @@ package com.expediagroup.graphql.generator.execution import com.expediagroup.graphql.generator.annotations.GraphQLName import com.expediagroup.graphql.generator.execution.GraphQLContext as KotlinGraphQLContext -import com.fasterxml.jackson.annotation.JsonProperty import graphql.GraphQLContext import graphql.GraphQLException import graphql.schema.DataFetchingEnvironment @@ -46,8 +45,6 @@ class FunctionDataFetcherTest { fun printDefault(string: String? = "hello") = string - fun printArray(items: Array) = items.joinToString(separator = ":") - fun printList(items: List) = items.joinToString(separator = ":") fun contextClass(myContext: MyContext) = myContext.value @@ -72,7 +69,7 @@ class FunctionDataFetcherTest { is OptionalInput.Defined -> "input was ${input.value}" } - fun optionalArrayInputObjects(input: OptionalInput>): String = when (input) { + fun optionalListInputObjects(input: OptionalInput>): String = when (input) { is OptionalInput.Undefined -> "input was UNDEFINED" is OptionalInput.Defined -> "first input was ${input.value?.first()?.field1}" } @@ -87,7 +84,6 @@ class FunctionDataFetcherTest { @GraphQLName("MyInputClassRenamed") data class MyInputClass( - @JsonProperty("jacksonField") @GraphQLName("jacksonField") val field1: String ) @@ -178,17 +174,7 @@ class FunctionDataFetcherTest { } @Test - fun `array inputs can be converted by the object mapper`() { - val dataFetcher = FunctionDataFetcher(target = MyClass(), fn = MyClass::printArray) - val mockEnvironment: DataFetchingEnvironment = mockk { - every { arguments } returns mapOf("items" to arrayOf("foo", "bar")) - every { containsArgument("items") } returns true - } - assertEquals(expected = "foo:bar", actual = dataFetcher.get(mockEnvironment)) - } - - @Test - fun `list can be converted by the object mapper`() { + fun `list inputs can be converted`() { val dataFetcher = FunctionDataFetcher(target = MyClass(), fn = MyClass::printList) val mockEnvironment: DataFetchingEnvironment = mockk { every { arguments } returns mapOf("items" to listOf("foo", "bar")) @@ -264,7 +250,7 @@ class FunctionDataFetcherTest { } @Test - fun `renamed fields can be converted by the object mapper`() { + fun `renamed fields can be converted`() { val dataFetcher = FunctionDataFetcher(target = MyClass(), fn = MyClass::renamedFields) val mockEnvironment: DataFetchingEnvironment = mockk { every { arguments } returns mapOf("myCustomArgument" to mapOf("jacksonField" to "foo")) @@ -304,10 +290,10 @@ class FunctionDataFetcherTest { } @Test - fun `optional array of input objects is deserialized correctly`() { - val dataFetcher = FunctionDataFetcher(target = MyClass(), fn = MyClass::optionalArrayInputObjects) + fun `optional list of input objects is deserialized correctly`() { + val dataFetcher = FunctionDataFetcher(target = MyClass(), fn = MyClass::optionalListInputObjects) val mockEnvironment: DataFetchingEnvironment = mockk { - every { arguments } returns mapOf("input" to arrayListOf(linkedMapOf("jacksonField" to "foo"))) + every { arguments } returns mapOf("input" to listOf(linkedMapOf("jacksonField" to "foo"))) every { containsArgument("input") } returns true } val result = dataFetcher.get(mockEnvironment) diff --git a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/extensions/KClassExtensionsTest.kt b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/extensions/KClassExtensionsTest.kt index 06b35ac743..9b1f171c11 100644 --- a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/extensions/KClassExtensionsTest.kt +++ b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/extensions/KClassExtensionsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -266,8 +266,6 @@ open class KClassExtensionsTest { @Test fun `test listType extension`() { - assertTrue(arrayOf(1)::class.isListType()) - assertTrue(intArrayOf(1)::class.isListType()) assertTrue(listOf(1)::class.isListType()) assertFalse(MyTestClass::class.isListType()) } diff --git a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/extensions/KTypeExtensionsKtTest.kt b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/extensions/KTypeExtensionsKtTest.kt index 8e327b6b94..39026816ac 100644 --- a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/extensions/KTypeExtensionsKtTest.kt +++ b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/extensions/KTypeExtensionsKtTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,8 +39,6 @@ class KTypeExtensionsKtTest { fun arrayFun(array: Array) = array.joinToString(separator = ",") { it } - fun primitiveArrayFun(intArray: IntArray) = intArray.joinToString(separator = ",") { it.toString() } - fun stringFun(string: String) = "hello $string" } @@ -54,8 +52,6 @@ class KTypeExtensionsKtTest { assertEquals(String::class.starProjectedType, MyClass::arrayFun.findParameterByName("array")?.type?.getTypeOfFirstArgument()) - assertEquals(Int::class.starProjectedType, MyClass::primitiveArrayFun.findParameterByName("intArray")?.type?.getWrappedType()) - assertFailsWith(InvalidWrappedTypeException::class) { MyClass::stringFun.findParameterByName("string")?.type?.getTypeOfFirstArgument() } @@ -88,9 +84,6 @@ class KTypeExtensionsKtTest { val arrayType = assertNotNull(MyClass::arrayFun.findParameterByName("array")?.type) assertEquals(Array::class.java, arrayType.getJavaClass()) - val primitiveArrayType = assertNotNull(MyClass::primitiveArrayFun.findParameterByName("intArray")?.type) - assertEquals(IntArray::class.java, primitiveArrayType.getJavaClass()) - val stringType = assertNotNull(MyClass::stringFun.findParameterByName("string")?.type) assertEquals(String::class.java, stringType.getJavaClass()) } @@ -127,22 +120,6 @@ class KTypeExtensionsKtTest { assertFalse(MyClass::class.starProjectedType.isListType()) } - @Test - fun getWrappedType() { - assertEquals(Int::class.starProjectedType, IntArray::class.starProjectedType.getWrappedType()) - assertEquals(Long::class.starProjectedType, LongArray::class.starProjectedType.getWrappedType()) - assertEquals(Short::class.starProjectedType, ShortArray::class.starProjectedType.getWrappedType()) - assertEquals(Float::class.starProjectedType, FloatArray::class.starProjectedType.getWrappedType()) - assertEquals(Double::class.starProjectedType, DoubleArray::class.starProjectedType.getWrappedType()) - assertEquals(Char::class.starProjectedType, CharArray::class.starProjectedType.getWrappedType()) - assertEquals(Boolean::class.starProjectedType, BooleanArray::class.starProjectedType.getWrappedType()) - assertEquals(String::class.starProjectedType, MyClass::listFun.findParameterByName("list")?.type?.getWrappedType()) - - assertFailsWith(InvalidWrappedTypeException::class) { - MyClass::stringFun.findParameterByName("string")?.type?.getWrappedType() - } - } - @Test fun getSimpleName() { assertEquals("MyClass", MyClass::class.starProjectedType.getSimpleName()) diff --git a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/state/TypesCacheTest.kt b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/state/TypesCacheTest.kt index 88577e8e4f..490254637c 100644 --- a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/state/TypesCacheTest.kt +++ b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/state/TypesCacheTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,10 +54,6 @@ class TypesCacheTest { fun objectListFun(list: List) = list.map { it.id.toString() }.joinToString(separator = ",") { it } - fun arrayFun(array: Array) = array.joinToString(separator = ",") { it } - - fun primitiveArrayFun(intArray: IntArray) = intArray.joinToString(separator = ",") { it.toString() } - @GraphQLUnion(name = "CustomUnion", possibleTypes = [MyType::class, Int::class]) fun customUnion(): Any = MyType(1) @@ -123,36 +119,6 @@ class TypesCacheTest { assertNull(cache.put(cacheKey, cacheValue)) } - @Test - fun `primitive array types are not cached`() { - val cache = TypesCache(listOf("com.expediagroup.graphql.generator")) - - val type = MyClass::primitiveArrayFun.findParameterByName("intArray")?.type - - assertNotNull(type) - - val cacheKey = TypesCacheKey(type) - val cacheValue = KGraphQLType(MyType::class, graphQLType) - - assertNull(cache.get(cacheKey)) - assertNull(cache.put(cacheKey, cacheValue)) - } - - @Test - fun `array types are not cached`() { - val cache = TypesCache(listOf("com.expediagroup.graphql.generator")) - - val type = MyClass::arrayFun.findParameterByName("array")?.type - - assertNotNull(type) - - val cacheKey = TypesCacheKey(type) - val cacheValue = KGraphQLType(MyType::class, graphQLType) - - assertNull(cache.get(cacheKey)) - assertNull(cache.put(cacheKey, cacheValue)) - } - @Test fun `custom unions are cached by special name`() { val cache = TypesCache(listOf("com.expediagroup.graphql.generator")) diff --git a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateArgumentTest.kt b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateArgumentTest.kt index 2a0eb8b873..832930f3a1 100644 --- a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateArgumentTest.kt +++ b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateArgumentTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,18 +51,10 @@ class GenerateArgumentTest : TypeTestHelper() { fun changeName(@GraphQLName("newName") input: String) = input - fun idClass(idArg: ID) = "Your id is ${idArg.value}" + fun idClass(idArg: ID) = "Your id is $idArg" fun interfaceArg(input: MyInterface) = input.id - fun arrayArg(input: IntArray) = input - - fun arrayListArg(input: ArrayList) = input - - fun arrayListInterfaceArg(input: ArrayList) = input - - fun arrayListUnionArg(input: ArrayList) = input - fun listArg(input: List) = input fun listInterfaceArg(input: List) = input @@ -136,46 +128,6 @@ class GenerateArgumentTest : TypeTestHelper() { } } - @Test - fun `Primitive array argument type is valid`() { - val kParameter = ArgumentTestClass::arrayArg.findParameterByName("input") - assertNotNull(kParameter) - val result = generateArgument(generator, kParameter) - - assertEquals(expected = "input", actual = result.name) - assertNotNull(GraphQLTypeUtil.unwrapNonNull(result.type) as? GraphQLList) - } - - @Test - fun `ArrayList argument type is valid`() { - val kParameter = ArgumentTestClass::arrayListArg.findParameterByName("input") - assertNotNull(kParameter) - val result = generateArgument(generator, kParameter) - - assertEquals(expected = "input", actual = result.name) - assertNotNull(GraphQLTypeUtil.unwrapNonNull(result.type) as? GraphQLList) - } - - @Test - fun `ArrayList of interfaces as input is invalid`() { - val kParameter = ArgumentTestClass::arrayListInterfaceArg.findParameterByName("input") - assertNotNull(kParameter) - - assertFailsWith(InvalidInputFieldTypeException::class) { - generateArgument(generator, kParameter) - } - } - - @Test - fun `ArrayList of unions as input is invalid`() { - val kParameter = ArgumentTestClass::arrayListUnionArg.findParameterByName("input") - assertNotNull(kParameter) - - assertFailsWith(InvalidInputFieldTypeException::class) { - generateArgument(generator, kParameter) - } - } - @Test fun `List argument type is valid`() { val kParameter = ArgumentTestClass::listArg.findParameterByName("input") diff --git a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateInputObjectTest.kt b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateInputObjectTest.kt index b2f95c02dd..43ab3a48fc 100644 --- a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateInputObjectTest.kt +++ b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateInputObjectTest.kt @@ -21,6 +21,7 @@ import com.expediagroup.graphql.generator.annotations.GraphQLName import com.expediagroup.graphql.generator.annotations.GraphQLValidObjectLocations import com.expediagroup.graphql.generator.exceptions.InvalidGraphQLNameException import com.expediagroup.graphql.generator.exceptions.InvalidObjectLocationException +import com.expediagroup.graphql.generator.exceptions.PrimaryConstructorNotFound import com.expediagroup.graphql.generator.test.utils.SimpleDirective import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow @@ -56,6 +57,8 @@ class GenerateInputObjectTest : TypeTestHelper() { class `Invalid$InputTypeName` + class MissingPublicConstructor private constructor(val id: Int) + @Test fun `Test naming`() { val result = generateInputObject(generator, InputClass::class) @@ -110,9 +113,16 @@ class GenerateInputObjectTest : TypeTestHelper() { } @Test - fun `Generation of output object will fail if it specifies invalid name`() { + fun `Generation of input object will fail if it specifies invalid name`() { assertFailsWith(InvalidGraphQLNameException::class) { generateInputObject(generator, `Invalid$InputTypeName`::class) } } + + @Test + fun `Generation of input object will fail if it does not have public constructor`() { + assertFailsWith(PrimaryConstructorNotFound::class) { + generateInputObject(generator, MissingPublicConstructor::class) + } + } } diff --git a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateListTest.kt b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateListTest.kt index ec008214d5..26538ce563 100644 --- a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateListTest.kt +++ b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateListTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,16 +26,14 @@ class GenerateListTest : TypeTestHelper() { private data class MyDataClass(val id: String) - private class ClassWithListAndArray { + private class ClassWithList { val testList: List = listOf(1) val testListOfClass: List = listOf(MyDataClass("bar")) - val testArray: Array = arrayOf("foo") - val primitiveArray: BooleanArray = booleanArrayOf(true) } @Test fun `verify a list of primitives for output`() { - val listProp = ClassWithListAndArray::testList + val listProp = ClassWithList::testList val result = generateList(generator, listProp.returnType, GraphQLKTypeMetadata()) assertEquals("Int", getListTypeName(result)) @@ -43,47 +41,15 @@ class GenerateListTest : TypeTestHelper() { @Test fun `verify a list of objects for ouput`() { - val listProp = ClassWithListAndArray::testListOfClass + val listProp = ClassWithList::testListOfClass val result = generateList(generator, listProp.returnType, GraphQLKTypeMetadata()) assertEquals("MyDataClass", getListTypeName(result)) } - @Test - fun `verify arrays are valid output`() { - val arrayProp = ClassWithListAndArray::testArray - - val result = generateList(generator, arrayProp.returnType, GraphQLKTypeMetadata()) - assertEquals("String", getListTypeName(result)) - } - - @Test - fun `verify primitive arrays are valid output`() { - val primitiveArray = ClassWithListAndArray::primitiveArray - - val result = generateList(generator, primitiveArray.returnType, GraphQLKTypeMetadata()) - assertEquals("Boolean", getListTypeName(result)) - } - - @Test - fun `verify arrays are valid input`() { - val arrayProp = ClassWithListAndArray::testArray - - val result = generateList(generator, arrayProp.returnType, GraphQLKTypeMetadata(inputType = true)) - assertEquals("String", getListTypeName(result)) - } - - @Test - fun `verify primitive arrays are valid input`() { - val primitiveArray = ClassWithListAndArray::primitiveArray - - val result = generateList(generator, primitiveArray.returnType, GraphQLKTypeMetadata(inputType = true)) - assertEquals("Boolean", getListTypeName(result)) - } - @Test fun `verify a list of primitives for input`() { - val listProp = ClassWithListAndArray::testList + val listProp = ClassWithList::testList val result = generateList(generator, listProp.returnType, GraphQLKTypeMetadata(inputType = true)) assertEquals("Int", getListTypeName(result)) @@ -91,7 +57,7 @@ class GenerateListTest : TypeTestHelper() { @Test fun `verify a list of objects for input`() { - val listProp = ClassWithListAndArray::testListOfClass + val listProp = ClassWithList::testListOfClass val result = generateList(generator, listProp.returnType, GraphQLKTypeMetadata(inputType = true)) assertEquals("MyDataClassInput", getListTypeName(result)) diff --git a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/scalars/IDTest.kt b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/scalars/IDTest.kt deleted file mode 100644 index 19ffda8fe5..0000000000 --- a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/scalars/IDTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.generator.scalars - -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import org.junit.jupiter.api.Test -import kotlin.test.assertEquals - -class IDTest { - - private val mapper = jacksonObjectMapper() - - @Test - fun testToString() { - val id = ID("1") - assertEquals(expected = "1", actual = id.toString()) - } - - @Test - fun serialization() { - val id = ID("2") - - val json = mapper.writeValueAsString(id) - - assertEquals(expected = "\"2\"", actual = json) - - val parsed: ID = mapper.readValue(json) - - assertEquals(expected = "2", actual = parsed.value) - } -} diff --git a/servers/graphql-kotlin-server/build.gradle.kts b/servers/graphql-kotlin-server/build.gradle.kts index 6843d01209..3e3f3b2467 100644 --- a/servers/graphql-kotlin-server/build.gradle.kts +++ b/servers/graphql-kotlin-server/build.gradle.kts @@ -9,8 +9,10 @@ plugins { id("org.jetbrains.kotlinx.benchmark") } +val jacksonVersion: String by project dependencies { api(project(path = ":graphql-kotlin-schema-generator")) + api("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion") } diff --git a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLExecutionConfiguration.kt b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLExecutionConfiguration.kt index 932097ec29..463489c7a5 100644 --- a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLExecutionConfiguration.kt +++ b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLExecutionConfiguration.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import com.expediagroup.graphql.server.execution.DataLoaderRegistryFactory import com.expediagroup.graphql.server.execution.DefaultDataLoaderRegistryFactory import com.expediagroup.graphql.server.execution.KotlinDataLoader import com.expediagroup.graphql.server.spring.execution.SpringKotlinDataFetcherFactoryProvider -import com.fasterxml.jackson.databind.ObjectMapper import graphql.execution.DataFetcherExceptionHandler import graphql.execution.SimpleDataFetcherExceptionHandler import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean @@ -44,8 +43,8 @@ class GraphQLExecutionConfiguration { @Bean @ConditionalOnMissingBean - fun dataFetcherFactoryProvider(objectMapper: ObjectMapper, applicationContext: ApplicationContext): KotlinDataFetcherFactoryProvider = - SpringKotlinDataFetcherFactoryProvider(objectMapper, applicationContext) + fun dataFetcherFactoryProvider(applicationContext: ApplicationContext): KotlinDataFetcherFactoryProvider = + SpringKotlinDataFetcherFactoryProvider(applicationContext) @Bean @ConditionalOnMissingBean diff --git a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/execution/SpringDataFetcher.kt b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/execution/SpringDataFetcher.kt index b309aba39d..928bae87dd 100644 --- a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/execution/SpringDataFetcher.kt +++ b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/execution/SpringDataFetcher.kt @@ -17,7 +17,6 @@ package com.expediagroup.graphql.server.spring.execution import com.expediagroup.graphql.generator.execution.FunctionDataFetcher -import com.fasterxml.jackson.databind.ObjectMapper import graphql.schema.DataFetchingEnvironment import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier @@ -34,9 +33,8 @@ import kotlin.reflect.jvm.javaType open class SpringDataFetcher( target: Any?, fn: KFunction<*>, - objectMapper: ObjectMapper, private val applicationContext: ApplicationContext -) : FunctionDataFetcher(target, fn, objectMapper) { +) : FunctionDataFetcher(target, fn) { override fun mapParameterToValue(param: KParameter, environment: DataFetchingEnvironment): Pair? = if (param.hasAnnotation()) { diff --git a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/execution/SpringKotlinDataFetcherFactoryProvider.kt b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/execution/SpringKotlinDataFetcherFactoryProvider.kt index 122186472f..2244b043d2 100644 --- a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/execution/SpringKotlinDataFetcherFactoryProvider.kt +++ b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/execution/SpringKotlinDataFetcherFactoryProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package com.expediagroup.graphql.server.spring.execution import com.expediagroup.graphql.generator.execution.SimpleKotlinDataFetcherFactoryProvider -import com.fasterxml.jackson.databind.ObjectMapper import graphql.schema.DataFetcherFactory import org.springframework.context.ApplicationContext import kotlin.reflect.KFunction @@ -27,9 +26,8 @@ import kotlin.reflect.KFunction * This allows you to use Spring beans as function arugments and they will be populated by the data fetcher. */ class SpringKotlinDataFetcherFactoryProvider( - private val objectMapper: ObjectMapper, private val applicationContext: ApplicationContext -) : SimpleKotlinDataFetcherFactoryProvider(objectMapper) { +) : SimpleKotlinDataFetcherFactoryProvider() { override fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>): DataFetcherFactory = - DataFetcherFactory { SpringDataFetcher(target, kFunction, objectMapper, applicationContext) } + DataFetcherFactory { SpringDataFetcher(target, kFunction, applicationContext) } } diff --git a/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/SchemaConfigurationTest.kt b/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/SchemaConfigurationTest.kt index 379f25a03d..11cf19e304 100644 --- a/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/SchemaConfigurationTest.kt +++ b/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/SchemaConfigurationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.expediagroup.graphql.generator.SchemaGeneratorConfig import com.expediagroup.graphql.generator.TopLevelObject import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.generator.execution.KotlinDataFetcherFactoryProvider -import com.expediagroup.graphql.generator.execution.SimpleKotlinDataFetcherFactoryProvider import com.expediagroup.graphql.generator.toSchema import com.expediagroup.graphql.server.execution.DataLoaderRegistryFactory import com.expediagroup.graphql.server.execution.GraphQLContextFactory @@ -68,11 +67,6 @@ class SchemaConfigurationTest { assertThat(ctx).hasSingleBean(ObjectMapper::class.java) val mapper = ctx.getBean(ObjectMapper::class.java) assertThat(ctx).hasSingleBean(KotlinDataFetcherFactoryProvider::class.java) - val dataFetcherFactoryProvider = ctx.getBean(KotlinDataFetcherFactoryProvider::class.java) - val privateMapperField = SimpleKotlinDataFetcherFactoryProvider::class.java.getDeclaredField("objectMapper") - privateMapperField.isAccessible = true - val privateMapper = privateMapperField.get(dataFetcherFactoryProvider) - assertEquals(mapper, privateMapper) assertThat(ctx).hasSingleBean(GraphQLSchema::class.java) val schema = ctx.getBean(GraphQLSchema::class.java) diff --git a/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/execution/SpringDataFetcherTest.kt b/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/execution/SpringDataFetcherTest.kt index 9381fe4973..56a07e0c41 100644 --- a/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/execution/SpringDataFetcherTest.kt +++ b/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/execution/SpringDataFetcherTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package com.expediagroup.graphql.server.spring.execution import com.expediagroup.graphql.generator.annotations.GraphQLIgnore -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import graphql.schema.DataFetchingEnvironment import io.mockk.every import io.mockk.mockk @@ -31,7 +30,6 @@ import kotlin.test.assertFailsWith class SpringDataFetcherTest { - private val objectMapper = jacksonObjectMapper() private val context: ApplicationContext = mockk { every { getBean(MyService::class.java) } returns MyService() every { getBean(NotService::class.java) } throws NoSuchBeanDefinitionException(NotService::class.java) @@ -45,7 +43,6 @@ class SpringDataFetcherTest { val dataFetcher = SpringDataFetcher( target = MyQuery(), fn = MyQuery::callService, - objectMapper = objectMapper, applicationContext = context ) @@ -59,7 +56,6 @@ class SpringDataFetcherTest { val dataFetcher = SpringDataFetcher( target = MyQuery(), fn = MyQuery::callServiceNoAnnotation, - objectMapper = objectMapper, applicationContext = context ) @@ -73,7 +69,6 @@ class SpringDataFetcherTest { val dataFetcher = SpringDataFetcher( target = MyQuery(), fn = MyQuery::callNotService, - objectMapper = objectMapper, applicationContext = context ) @@ -87,7 +82,6 @@ class SpringDataFetcherTest { val dataFetcher = SpringDataFetcher( target = MyQuery(), fn = MyQuery::callNotServiceNoAnnotation, - objectMapper = objectMapper, applicationContext = context ) diff --git a/website/docs/schema-generator/writing-schemas/arguments.md b/website/docs/schema-generator/writing-schemas/arguments.md index 032c939bb0..175eb5f744 100644 --- a/website/docs/schema-generator/writing-schemas/arguments.md +++ b/website/docs/schema-generator/writing-schemas/arguments.md @@ -63,6 +63,11 @@ input WidgetInput { Note that only fields are exposed in the input objects. Functions will only be available on the GraphQL output types. +:::caution +All input object fields have to be exposed through a public primary constructor. This primary constructor is used to instantiate +the input objects at runtime when resolving the queries. +::: + If you know a type will only be used for input types you can call your class something like `CustomTypeInput`. The library will not append `Input` if the class name already ends with `Input` but that means you can not use this type as output because the schema would have two types with the same name and that would be invalid. diff --git a/website/docs/schema-generator/writing-schemas/lists.md b/website/docs/schema-generator/writing-schemas/lists.md index 5adb49b291..108fbb4491 100644 --- a/website/docs/schema-generator/writing-schemas/lists.md +++ b/website/docs/schema-generator/writing-schemas/lists.md @@ -2,21 +2,15 @@ id: lists title: Lists --- -Both `kotlin.Array` and `kotlin.collections.List` are automatically mapped to the GraphQL `List` type (for unsupported -use cases see below). Type arguments provided to Kotlin collections are used as the type arguments in the GraphQL `List` -type. Kotlin specialized classes (e.g. `IntArray`) representing arrays of Java primitive types without boxing overhead -are also supported. +`kotlin.collections.List` is automatically mapped to the GraphQL `List` type. Type arguments provided to Kotlin collections +are used as the type arguments in the GraphQL `List` type. ```kotlin class SimpleQuery { - fun generateList(): List { + fun generateList(): List { // some logic here that generates list } - fun doSomethingWithIntArray(ints: IntArray): String { - // some logic here that processes array - } - fun doSomethingWithIntList(ints: List): String { // some logic here that processes list } @@ -27,38 +21,19 @@ The above Kotlin class would produce the following GraphQL schema: ```graphql type Query { - generateList: [Int!]! - doSomethingWithIntArray(ints: [Int!]!): String! + generateList: [String!]! doSomethingWithIntList(ints: [Int!]!): String! } ``` -## Primitive Arrays - -`graphql-kotlin-schema-generator` supports the following primitive array types without autoboxing overhead. Similarly to -the `kotlin.Array` of objects the underlying type is automatically mapped to GraphQL `List` type. - -| Kotlin Type | -| ---------------------------- | -| `kotlin.IntArray` | -| `kotlin.LongArray` | -| `kotlin.ShortArray` | -| `kotlin.FloatArray` | -| `kotlin.DoubleArray` | -| `kotlin.CharArray` | -| `kotlin.BooleanArray` | - -:::note -The underlying GraphQL types of primitive arrays will be corresponding to the built-in scalar types provided by `graphql-java`. -::: - -## Unsupported Collection Types +## Arrays and Unsupported Collection Types Currently, the GraphQL spec only supports `Lists`. Therefore, even though Java and Kotlin support number of other collection -types, `graphql-kotlin-schema-generator` only explicitly supports `Lists` and primitive arrays. Other collection types -such as `Sets` (see [#201](https://github.com/ExpediaGroup/graphql-kotlin/issues/201)) and arbitrary `Map` data -structures are not supported out of the box. While we do not reccomend using `Map` or `Set` in the schema, -they are supported with the use of the schema hooks. +types, `graphql-kotlin-schema-generator` only explicitly supports `Lists`. Other collection types such as `Sets` (see [#201](https://github.com/ExpediaGroup/graphql-kotlin/issues/201)) +and arbitrary `Map` data structures are not supported out of the box. While we do not recommend using `Map` or `Set` in the schema, +they could be supported with the use of the schema hooks. + +Due to the [argument deserialization issues](https://github.com/ExpediaGroup/graphql-kotlin/pull/1379), arrays are currently not supported ```kotlin override fun willResolveMonad(type: KType): KType = when (type.classifier) { diff --git a/website/docs/schema-generator/writing-schemas/scalars.md b/website/docs/schema-generator/writing-schemas/scalars.md index a3f8025497..becc7bfda1 100644 --- a/website/docs/schema-generator/writing-schemas/scalars.md +++ b/website/docs/schema-generator/writing-schemas/scalars.md @@ -22,10 +22,11 @@ The GraphQL spec uses the term `Float` for signed double‐precision fractional ## GraphQL ID GraphQL supports the scalar type `ID`, a unique identifier that is not intended to be human readable. IDs are -serialized as a `String`. To expose a GraphQL `ID` field, you must use the `com.expediagroup.graphql.generator.scalars.ID` class, which wraps the underlying `String` value. +serialized as a `String`. To expose a GraphQL `ID` field, you must use the `com.expediagroup.graphql.generator.scalars.ID` +class, which is an *inline value class* that wraps the underlying `String` value. :::note -`graphql-java` supports additional types (`String`, `Int`, `Long`, or `UUID`) but [due to serialization issues](https://github.com/ExpediaGroup/graphql-kotlin/issues/317) we can only directly support Strings. You can still use a type like UUID internally just as long as you convert or parse the value yourself and handle the errors. +`graphql-java` supports additional types (`String`, `Int`, `Long`, or `UUID`) but [due to serialization issues](https://github.com/ExpediaGroup/graphql-kotlin/issues/317) we can only directly support Strings. ::: ```kotlin