Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[generator] avoid duplicate argument deserialization #1379

Merged
merged 12 commits into from
Mar 8, 2022
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand All @@ -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
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -94,3 +101,36 @@ private object UUIDCoercing : Coercing<UUID, String> {
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<LocalDate>

private object PeriodCoercing : Coercing<Period, String> {
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])
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<Int>? = null,

@GraphQLIgnore
val ignoredField: String? = "ignored",

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -34,4 +34,14 @@ class WidgetMutation : Mutation {
}
return widget
}

fun processWidgetList(widgets: List<Widget>): List<Widget> {
widgets.forEach {
if (null == it.value) {
it.value = 42
}
}

return widgets
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand All @@ -32,11 +33,21 @@ 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<UUID>) = "You sent $uuids"

fun findPersonById(id: ID) = Person(id, "Nelson")

@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,
)
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -162,36 +162,6 @@ class SimpleQueryIT(@Autowired private val testClient: WebTestClient) {
.jsonPath("$DATA_JSON_PATH.$query").value(hasSize<Int>(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<Int>(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"
Expand Down
2 changes: 0 additions & 2 deletions generator/graphql-kotlin-schema-generator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Loading