Skip to content

Commit

Permalink
[client] generate enums in UPPERCASE and fix kotlinx request serializ…
Browse files Browse the repository at this point in the history
…ation (#1087)

Update client generation logic to always generate enums in UPPERCASE and then use corresponding Jackson (`@JsonProperty`) or kotlinx-serialization (`@SerialName`) annotation to convert those back to expected schema values. This allows us to support enum values that cannot be used directly in Kotlin (i.e. `name` and `ordinal`) as they would clash with built in methods.

This PR also resolves incorrect serialization of requests when using `kotlinx-serialization`. We were applying our custom `AnySerializer` to serialize the requests which was in turn incorrectly serializing enums and custom scalars. I changed the logic to construct correct `KSerializer` based on the specified request.

Resolves: #1075
  • Loading branch information
dariuszkuc authored Mar 24, 2021
1 parent 09b0682 commit 01db43b
Show file tree
Hide file tree
Showing 22 changed files with 351 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.expediagroup.graphql.client.jackson

import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLResponse
import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.ObjectMapper
Expand All @@ -35,7 +36,9 @@ class GraphQLClientJacksonSerializer(private val mapper: ObjectMapper = jacksonO
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
}

override fun serialize(request: Any): String = mapper.writeValueAsString(request)
override fun serialize(request: GraphQLClientRequest<*>): String = mapper.writeValueAsString(request)

override fun serialize(requests: List<GraphQLClientRequest<*>>): String = mapper.writeValueAsString(requests)

override fun <T : Any> deserialize(rawResponse: String, responseType: KClass<T>): JacksonGraphQLResponse<T> =
mapper.readValue(rawResponse, parameterizedType(responseType))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,21 @@ import com.expediagroup.graphql.client.jackson.data.FirstQuery
import com.expediagroup.graphql.client.jackson.data.OtherQuery
import com.expediagroup.graphql.client.jackson.data.PolymorphicQuery
import com.expediagroup.graphql.client.jackson.data.ScalarQuery
import com.expediagroup.graphql.client.jackson.data.enums.TestEnum
import com.expediagroup.graphql.client.jackson.data.polymorphicquery.SecondInterfaceImplementation
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLError
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLResponse
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLSourceLocation
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Test
import java.util.UUID
import kotlin.test.assertEquals

class GraphQLClientJacksonSerializerTest {

private val testMapper = jacksonObjectMapper()
private val serializer = GraphQLClientJacksonSerializer(testMapper)

@Test
fun `verify we can serialize GraphQLClientRequest`() {
val testQuery = FirstQuery(FirstQuery.Variables(input = 1.0f))
Expand All @@ -42,10 +47,8 @@ class GraphQLClientJacksonSerializerTest {
|}
""".trimMargin()

val mapper = jacksonObjectMapper()
val serializer = GraphQLClientJacksonSerializer(mapper)
val result = serializer.serialize(testQuery)
assertEquals(mapper.readTree(expected), mapper.readTree(result))
assertEquals(testMapper.readTree(expected), testMapper.readTree(result))
}

@Test
Expand All @@ -63,29 +66,56 @@ class GraphQLClientJacksonSerializerTest {
|}]
""".trimMargin()

val mapper = jacksonObjectMapper()
val serializer = GraphQLClientJacksonSerializer(mapper)
val result = serializer.serialize(queries)
assertEquals(mapper.readTree(expected), mapper.readTree(result))
assertEquals(testMapper.readTree(expected), testMapper.readTree(result))
}

@Test
fun `verify we can deserialize JacksonGraphQLResponse`() {
val testQuery = FirstQuery(variables = FirstQuery.Variables())
val expected = JacksonGraphQLResponse(
data = FirstQuery.Result("hello world"),
errors = listOf(JacksonGraphQLError(message = "test error message")),
extensions = mapOf("extVal" to 123, "extList" to listOf("ext1", "ext2"), "extMap" to mapOf("1" to 1, "2" to 2))
errors = listOf(
JacksonGraphQLError(
message = "test error message",
locations = listOf(JacksonGraphQLSourceLocation(1, 1)),
path = listOf("firstQuery", 0),
extensions = mapOf("errorExt" to 123)
)
),
extensions = mapOf(
"extBool" to true,
"extDouble" to 1.5,
"extInt" to 123,
"extList" to listOf("ext1", "ext2"),
"extMap" to mapOf("1" to 1, "2" to 2.0),
"extNull" to null,
"extString" to "extra"
)
)
val rawResponse =
"""{
| "data": { "stringResult" : "hello world" },
| "errors": [{ "message" : "test error message" }],
| "extensions" : { "extVal" : 123, "extList" : ["ext1", "ext2"], "extMap" : { "1" : 1, "2" : 2} }
| "errors": [{
| "message": "test error message",
| "locations": [{ "line": 1, "column": 1 }],
| "path": [ "firstQuery", 0 ],
| "extensions": {
| "errorExt": 123
| }
| }],
| "extensions" : {
| "extBool": true,
| "extDouble": 1.5,
| "extInt": 123,
| "extList": ["ext1", "ext2"],
| "extMap": { "1" : 1, "2" : 2.0 },
| "extNull": null,
| "extString": "extra"
| }
|}
""".trimMargin()

val serializer = GraphQLClientJacksonSerializer()
val result = serializer.deserialize(rawResponse, testQuery.responseType())
assertEquals(expected, result)
}
Expand Down Expand Up @@ -114,7 +144,6 @@ class GraphQLClientJacksonSerializerTest {
|}]
""".trimMargin()

val serializer = GraphQLClientJacksonSerializer()
val result = serializer.deserialize(rawResponses, listOf(testQuery.responseType(), otherQuery.responseType()))
assertEquals(expected, result)
}
Expand All @@ -132,11 +161,27 @@ class GraphQLClientJacksonSerializerTest {
| }
|}
""".trimMargin()
val serializer = GraphQLClientJacksonSerializer()

val result = serializer.deserialize(polymorphicResponse, PolymorphicQuery().responseType())
assertEquals(SecondInterfaceImplementation(123, 1.2f), result.data?.polymorphicResult)
}

@Test
fun `verify we can serialize custom scalars`() {
val randomUUID = UUID.randomUUID()
val scalarQuery = ScalarQuery(variables = ScalarQuery.Variables(alias = "1234", custom = com.expediagroup.graphql.client.jackson.data.scalars.UUID(randomUUID)))
val rawQuery =
"""{
| "query": "SCALAR_QUERY",
| "operationName": "ScalarQuery",
| "variables": { "alias": "1234", "custom": "$randomUUID" }
|}
""".trimMargin()

val serialized = serializer.serialize(scalarQuery)
assertEquals(testMapper.readTree(rawQuery), testMapper.readTree(serialized))
}

@Test
fun `verify we can deserialize custom scalars`() {
val expectedUUID = UUID.randomUUID()
Expand All @@ -148,8 +193,8 @@ class GraphQLClientJacksonSerializerTest {
| }
|}
""".trimMargin()
val serializer = GraphQLClientJacksonSerializer()
val result = serializer.deserialize(scalarResponse, ScalarQuery().responseType())

val result = serializer.deserialize(scalarResponse, ScalarQuery(ScalarQuery.Variables()).responseType())
assertEquals("1234", result.data?.scalarAlias)
assertEquals(expectedUUID, result.data?.customScalar?.value)
}
Expand All @@ -162,8 +207,33 @@ class GraphQLClientJacksonSerializerTest {
|}
""".trimMargin()

val serializer = GraphQLClientJacksonSerializer()
val result = serializer.deserialize(unknownResponse, EnumQuery().responseType())
assertEquals(EnumQuery.TestEnum.__UNKNOWN, result.data?.enumResult)
val result = serializer.deserialize(unknownResponse, EnumQuery(EnumQuery.Variables()).responseType())
assertEquals(TestEnum.__UNKNOWN, result.data?.enumResult)
}

@Test
fun `verify we can serialize enums with custom names`() {
val query = EnumQuery(variables = EnumQuery.Variables(enum = TestEnum.THREE))
val rawQuery =
"""{
| "query": "ENUM_QUERY",
| "operationName": "EnumQuery",
| "variables": { "enum": "three" }
|}
""".trimMargin()

val serialized = serializer.serialize(query)
assertEquals(testMapper.readTree(rawQuery), testMapper.readTree(serialized))
}

@Test
fun `verify we can deserialize enums with custom names`() {
val rawResponse =
"""{
| "data": { "enumResult": "three" }
|}
""".trimMargin()
val deserialized = serializer.deserialize(rawResponse, EnumQuery(EnumQuery.Variables()).responseType())
assertEquals(TestEnum.THREE, deserialized.data?.enumResult)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,22 @@

package com.expediagroup.graphql.client.jackson.data

import com.expediagroup.graphql.client.jackson.data.enums.TestEnum
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue
import kotlin.reflect.KClass

class EnumQuery : GraphQLClientRequest<EnumQuery.Result> {
class EnumQuery(
override val variables: Variables
) : GraphQLClientRequest<EnumQuery.Result> {
override val query: String = "ENUM_QUERY"

override val operationName: String = "EnumQuery"

override fun responseType(): KClass<Result> = Result::class

enum class TestEnum {
ONE,
TWO,
@JsonEnumDefaultValue
__UNKNOWN
}
data class Variables(
val enum: TestEnum? = null
)

data class Result(
val enumResult: TestEnum = TestEnum.__UNKNOWN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ import kotlin.reflect.KClass
// typealiases would be in separate file
typealias ID = String

class ScalarQuery : GraphQLClientRequest<ScalarQuery.Result> {
class ScalarQuery(
override val variables: Variables
) : GraphQLClientRequest<ScalarQuery.Result> {
override val query: String = "SCALAR_QUERY"

override val operationName: String = "ScalarQuery"

override fun responseType(): KClass<Result> = Result::class

data class Variables(
val alias: ID? = null,
val custom: UUID? = null
)

data class Result(
val scalarAlias: ID,
val customScalar: UUID
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2021 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.client.jackson.data.enums

import com.fasterxml.jackson.annotation.JsonEnumDefaultValue
import com.fasterxml.jackson.annotation.JsonProperty

enum class TestEnum {
ONE,
TWO,
@JsonProperty("three")
THREE,
@JsonEnumDefaultValue
__UNKNOWN
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ tasks {
limit {
counter = "INSTRUCTION"
value = "COVEREDRATIO"
minimum = "0.75".toBigDecimal()
minimum = "0.73".toBigDecimal()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

package com.expediagroup.graphql.client.serialization

import com.expediagroup.graphql.client.serialization.serializers.AnyKSerializer
import com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLResponse
import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
Expand All @@ -33,7 +33,8 @@ import kotlin.reflect.full.createType
*/
class GraphQLClientKotlinxSerializer(private val jsonBuilder: JsonBuilder.() -> Unit = {}) : GraphQLClientSerializer {

private val serializerCache = ConcurrentHashMap<KClass<*>, KSerializer<KotlinxGraphQLResponse<Any?>>>()
private val responseSerializerCache = ConcurrentHashMap<KClass<*>, KSerializer<KotlinxGraphQLResponse<Any?>>>()
private val requestSerializerCache = ConcurrentHashMap<KClass<*>, KSerializer<Any?>>()

private val json = Json {
ignoreUnknownKeys = true
Expand All @@ -43,7 +44,14 @@ class GraphQLClientKotlinxSerializer(private val jsonBuilder: JsonBuilder.() ->
encodeDefaults = true
}

override fun serialize(request: Any): String = json.encodeToString(AnyKSerializer, request)
override fun serialize(request: GraphQLClientRequest<*>): String = json.encodeToString(requestSerializer(request), request)

override fun serialize(requests: List<GraphQLClientRequest<*>>): String {
val serializedRequests = requests.map { request ->
json.encodeToString(requestSerializer(request), request)
}
return "[${serializedRequests.joinToString(",")}]"
}

override fun <T : Any> deserialize(rawResponse: String, responseType: KClass<T>): KotlinxGraphQLResponse<T> = json.decodeFromString(
responseSerializer(responseType) as KSerializer<KotlinxGraphQLResponse<T>>,
Expand All @@ -64,9 +72,14 @@ class GraphQLClientKotlinxSerializer(private val jsonBuilder: JsonBuilder.() ->
}
}

private fun requestSerializer(request: GraphQLClientRequest<*>): KSerializer<Any?> =
requestSerializerCache.computeIfAbsent(request::class) {
json.serializersModule.serializer(request::class.createType())
}

private fun <T : Any> responseSerializer(resultType: KClass<T>): KSerializer<KotlinxGraphQLResponse<Any?>> =
serializerCache.computeIfAbsent(resultType) {
val resultTypeSerializer = serializer(resultType.createType())
responseSerializerCache.computeIfAbsent(resultType) {
val resultTypeSerializer = json.serializersModule.serializer(resultType.createType())
KotlinxGraphQLResponse.serializer(
resultTypeSerializer
)
Expand Down
Loading

0 comments on commit 01db43b

Please sign in to comment.