Skip to content

Commit

Permalink
[generator] avoid duplicate argument deserialization (ExpediaGroup#1379)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dariusz Kuc committed Mar 8, 2022
1 parent a00bd83 commit ef57871
Show file tree
Hide file tree
Showing 42 changed files with 485 additions and 526 deletions.
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

0 comments on commit ef57871

Please sign in to comment.