Skip to content

Commit 01db43b

Browse files
authored
[client] generate enums in UPPERCASE and fix kotlinx request serialization (#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
1 parent 09b0682 commit 01db43b

File tree

22 files changed

+351
-114
lines changed

22 files changed

+351
-114
lines changed

clients/graphql-kotlin-client-jackson/src/main/kotlin/com/expediagroup/graphql/client/jackson/GraphQLClientJacksonSerializer.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.expediagroup.graphql.client.jackson
1818

1919
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLResponse
2020
import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer
21+
import com.expediagroup.graphql.client.types.GraphQLClientRequest
2122
import com.fasterxml.jackson.databind.DeserializationFeature
2223
import com.fasterxml.jackson.databind.JavaType
2324
import com.fasterxml.jackson.databind.ObjectMapper
@@ -35,7 +36,9 @@ class GraphQLClientJacksonSerializer(private val mapper: ObjectMapper = jacksonO
3536
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
3637
}
3738

38-
override fun serialize(request: Any): String = mapper.writeValueAsString(request)
39+
override fun serialize(request: GraphQLClientRequest<*>): String = mapper.writeValueAsString(request)
40+
41+
override fun serialize(requests: List<GraphQLClientRequest<*>>): String = mapper.writeValueAsString(requests)
3942

4043
override fun <T : Any> deserialize(rawResponse: String, responseType: KClass<T>): JacksonGraphQLResponse<T> =
4144
mapper.readValue(rawResponse, parameterizedType(responseType))

clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/GraphQLClientJacksonSerializerTest.kt

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,21 @@ import com.expediagroup.graphql.client.jackson.data.FirstQuery
2121
import com.expediagroup.graphql.client.jackson.data.OtherQuery
2222
import com.expediagroup.graphql.client.jackson.data.PolymorphicQuery
2323
import com.expediagroup.graphql.client.jackson.data.ScalarQuery
24+
import com.expediagroup.graphql.client.jackson.data.enums.TestEnum
2425
import com.expediagroup.graphql.client.jackson.data.polymorphicquery.SecondInterfaceImplementation
2526
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLError
2627
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLResponse
28+
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLSourceLocation
2729
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
2830
import org.junit.jupiter.api.Test
2931
import java.util.UUID
3032
import kotlin.test.assertEquals
3133

3234
class GraphQLClientJacksonSerializerTest {
3335

36+
private val testMapper = jacksonObjectMapper()
37+
private val serializer = GraphQLClientJacksonSerializer(testMapper)
38+
3439
@Test
3540
fun `verify we can serialize GraphQLClientRequest`() {
3641
val testQuery = FirstQuery(FirstQuery.Variables(input = 1.0f))
@@ -42,10 +47,8 @@ class GraphQLClientJacksonSerializerTest {
4247
|}
4348
""".trimMargin()
4449

45-
val mapper = jacksonObjectMapper()
46-
val serializer = GraphQLClientJacksonSerializer(mapper)
4750
val result = serializer.serialize(testQuery)
48-
assertEquals(mapper.readTree(expected), mapper.readTree(result))
51+
assertEquals(testMapper.readTree(expected), testMapper.readTree(result))
4952
}
5053

5154
@Test
@@ -63,29 +66,56 @@ class GraphQLClientJacksonSerializerTest {
6366
|}]
6467
""".trimMargin()
6568

66-
val mapper = jacksonObjectMapper()
67-
val serializer = GraphQLClientJacksonSerializer(mapper)
6869
val result = serializer.serialize(queries)
69-
assertEquals(mapper.readTree(expected), mapper.readTree(result))
70+
assertEquals(testMapper.readTree(expected), testMapper.readTree(result))
7071
}
7172

7273
@Test
7374
fun `verify we can deserialize JacksonGraphQLResponse`() {
7475
val testQuery = FirstQuery(variables = FirstQuery.Variables())
7576
val expected = JacksonGraphQLResponse(
7677
data = FirstQuery.Result("hello world"),
77-
errors = listOf(JacksonGraphQLError(message = "test error message")),
78-
extensions = mapOf("extVal" to 123, "extList" to listOf("ext1", "ext2"), "extMap" to mapOf("1" to 1, "2" to 2))
78+
errors = listOf(
79+
JacksonGraphQLError(
80+
message = "test error message",
81+
locations = listOf(JacksonGraphQLSourceLocation(1, 1)),
82+
path = listOf("firstQuery", 0),
83+
extensions = mapOf("errorExt" to 123)
84+
)
85+
),
86+
extensions = mapOf(
87+
"extBool" to true,
88+
"extDouble" to 1.5,
89+
"extInt" to 123,
90+
"extList" to listOf("ext1", "ext2"),
91+
"extMap" to mapOf("1" to 1, "2" to 2.0),
92+
"extNull" to null,
93+
"extString" to "extra"
94+
)
7995
)
8096
val rawResponse =
8197
"""{
8298
| "data": { "stringResult" : "hello world" },
83-
| "errors": [{ "message" : "test error message" }],
84-
| "extensions" : { "extVal" : 123, "extList" : ["ext1", "ext2"], "extMap" : { "1" : 1, "2" : 2} }
99+
| "errors": [{
100+
| "message": "test error message",
101+
| "locations": [{ "line": 1, "column": 1 }],
102+
| "path": [ "firstQuery", 0 ],
103+
| "extensions": {
104+
| "errorExt": 123
105+
| }
106+
| }],
107+
| "extensions" : {
108+
| "extBool": true,
109+
| "extDouble": 1.5,
110+
| "extInt": 123,
111+
| "extList": ["ext1", "ext2"],
112+
| "extMap": { "1" : 1, "2" : 2.0 },
113+
| "extNull": null,
114+
| "extString": "extra"
115+
| }
85116
|}
86117
""".trimMargin()
87118

88-
val serializer = GraphQLClientJacksonSerializer()
89119
val result = serializer.deserialize(rawResponse, testQuery.responseType())
90120
assertEquals(expected, result)
91121
}
@@ -114,7 +144,6 @@ class GraphQLClientJacksonSerializerTest {
114144
|}]
115145
""".trimMargin()
116146

117-
val serializer = GraphQLClientJacksonSerializer()
118147
val result = serializer.deserialize(rawResponses, listOf(testQuery.responseType(), otherQuery.responseType()))
119148
assertEquals(expected, result)
120149
}
@@ -132,11 +161,27 @@ class GraphQLClientJacksonSerializerTest {
132161
| }
133162
|}
134163
""".trimMargin()
135-
val serializer = GraphQLClientJacksonSerializer()
164+
136165
val result = serializer.deserialize(polymorphicResponse, PolymorphicQuery().responseType())
137166
assertEquals(SecondInterfaceImplementation(123, 1.2f), result.data?.polymorphicResult)
138167
}
139168

169+
@Test
170+
fun `verify we can serialize custom scalars`() {
171+
val randomUUID = UUID.randomUUID()
172+
val scalarQuery = ScalarQuery(variables = ScalarQuery.Variables(alias = "1234", custom = com.expediagroup.graphql.client.jackson.data.scalars.UUID(randomUUID)))
173+
val rawQuery =
174+
"""{
175+
| "query": "SCALAR_QUERY",
176+
| "operationName": "ScalarQuery",
177+
| "variables": { "alias": "1234", "custom": "$randomUUID" }
178+
|}
179+
""".trimMargin()
180+
181+
val serialized = serializer.serialize(scalarQuery)
182+
assertEquals(testMapper.readTree(rawQuery), testMapper.readTree(serialized))
183+
}
184+
140185
@Test
141186
fun `verify we can deserialize custom scalars`() {
142187
val expectedUUID = UUID.randomUUID()
@@ -148,8 +193,8 @@ class GraphQLClientJacksonSerializerTest {
148193
| }
149194
|}
150195
""".trimMargin()
151-
val serializer = GraphQLClientJacksonSerializer()
152-
val result = serializer.deserialize(scalarResponse, ScalarQuery().responseType())
196+
197+
val result = serializer.deserialize(scalarResponse, ScalarQuery(ScalarQuery.Variables()).responseType())
153198
assertEquals("1234", result.data?.scalarAlias)
154199
assertEquals(expectedUUID, result.data?.customScalar?.value)
155200
}
@@ -162,8 +207,33 @@ class GraphQLClientJacksonSerializerTest {
162207
|}
163208
""".trimMargin()
164209

165-
val serializer = GraphQLClientJacksonSerializer()
166-
val result = serializer.deserialize(unknownResponse, EnumQuery().responseType())
167-
assertEquals(EnumQuery.TestEnum.__UNKNOWN, result.data?.enumResult)
210+
val result = serializer.deserialize(unknownResponse, EnumQuery(EnumQuery.Variables()).responseType())
211+
assertEquals(TestEnum.__UNKNOWN, result.data?.enumResult)
212+
}
213+
214+
@Test
215+
fun `verify we can serialize enums with custom names`() {
216+
val query = EnumQuery(variables = EnumQuery.Variables(enum = TestEnum.THREE))
217+
val rawQuery =
218+
"""{
219+
| "query": "ENUM_QUERY",
220+
| "operationName": "EnumQuery",
221+
| "variables": { "enum": "three" }
222+
|}
223+
""".trimMargin()
224+
225+
val serialized = serializer.serialize(query)
226+
assertEquals(testMapper.readTree(rawQuery), testMapper.readTree(serialized))
227+
}
228+
229+
@Test
230+
fun `verify we can deserialize enums with custom names`() {
231+
val rawResponse =
232+
"""{
233+
| "data": { "enumResult": "three" }
234+
|}
235+
""".trimMargin()
236+
val deserialized = serializer.deserialize(rawResponse, EnumQuery(EnumQuery.Variables()).responseType())
237+
assertEquals(TestEnum.THREE, deserialized.data?.enumResult)
168238
}
169239
}

clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/EnumQuery.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,22 @@
1616

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

19+
import com.expediagroup.graphql.client.jackson.data.enums.TestEnum
1920
import com.expediagroup.graphql.client.types.GraphQLClientRequest
20-
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue
2121
import kotlin.reflect.KClass
2222

23-
class EnumQuery : GraphQLClientRequest<EnumQuery.Result> {
23+
class EnumQuery(
24+
override val variables: Variables
25+
) : GraphQLClientRequest<EnumQuery.Result> {
2426
override val query: String = "ENUM_QUERY"
2527

2628
override val operationName: String = "EnumQuery"
2729

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

30-
enum class TestEnum {
31-
ONE,
32-
TWO,
33-
@JsonEnumDefaultValue
34-
__UNKNOWN
35-
}
32+
data class Variables(
33+
val enum: TestEnum? = null
34+
)
3635

3736
data class Result(
3837
val enumResult: TestEnum = TestEnum.__UNKNOWN

clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/ScalarQuery.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,20 @@ import kotlin.reflect.KClass
2323
// typealiases would be in separate file
2424
typealias ID = String
2525

26-
class ScalarQuery : GraphQLClientRequest<ScalarQuery.Result> {
26+
class ScalarQuery(
27+
override val variables: Variables
28+
) : GraphQLClientRequest<ScalarQuery.Result> {
2729
override val query: String = "SCALAR_QUERY"
2830

2931
override val operationName: String = "ScalarQuery"
3032

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

35+
data class Variables(
36+
val alias: ID? = null,
37+
val custom: UUID? = null
38+
)
39+
3340
data class Result(
3441
val scalarAlias: ID,
3542
val customScalar: UUID
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2021 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.client.jackson.data.enums
18+
19+
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue
20+
import com.fasterxml.jackson.annotation.JsonProperty
21+
22+
enum class TestEnum {
23+
ONE,
24+
TWO,
25+
@JsonProperty("three")
26+
THREE,
27+
@JsonEnumDefaultValue
28+
__UNKNOWN
29+
}

clients/graphql-kotlin-client-serialization/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ tasks {
2020
limit {
2121
counter = "INSTRUCTION"
2222
value = "COVEREDRATIO"
23-
minimum = "0.75".toBigDecimal()
23+
minimum = "0.73".toBigDecimal()
2424
}
2525
}
2626
}

clients/graphql-kotlin-client-serialization/src/main/kotlin/com/expediagroup/graphql/client/serialization/GraphQLClientKotlinxSerializer.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
package com.expediagroup.graphql.client.serialization
1818

19-
import com.expediagroup.graphql.client.serialization.serializers.AnyKSerializer
2019
import com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLResponse
2120
import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer
21+
import com.expediagroup.graphql.client.types.GraphQLClientRequest
2222
import kotlinx.serialization.KSerializer
2323
import kotlinx.serialization.json.Json
2424
import kotlinx.serialization.json.JsonArray
@@ -33,7 +33,8 @@ import kotlin.reflect.full.createType
3333
*/
3434
class GraphQLClientKotlinxSerializer(private val jsonBuilder: JsonBuilder.() -> Unit = {}) : GraphQLClientSerializer {
3535

36-
private val serializerCache = ConcurrentHashMap<KClass<*>, KSerializer<KotlinxGraphQLResponse<Any?>>>()
36+
private val responseSerializerCache = ConcurrentHashMap<KClass<*>, KSerializer<KotlinxGraphQLResponse<Any?>>>()
37+
private val requestSerializerCache = ConcurrentHashMap<KClass<*>, KSerializer<Any?>>()
3738

3839
private val json = Json {
3940
ignoreUnknownKeys = true
@@ -43,7 +44,14 @@ class GraphQLClientKotlinxSerializer(private val jsonBuilder: JsonBuilder.() ->
4344
encodeDefaults = true
4445
}
4546

46-
override fun serialize(request: Any): String = json.encodeToString(AnyKSerializer, request)
47+
override fun serialize(request: GraphQLClientRequest<*>): String = json.encodeToString(requestSerializer(request), request)
48+
49+
override fun serialize(requests: List<GraphQLClientRequest<*>>): String {
50+
val serializedRequests = requests.map { request ->
51+
json.encodeToString(requestSerializer(request), request)
52+
}
53+
return "[${serializedRequests.joinToString(",")}]"
54+
}
4755

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

75+
private fun requestSerializer(request: GraphQLClientRequest<*>): KSerializer<Any?> =
76+
requestSerializerCache.computeIfAbsent(request::class) {
77+
json.serializersModule.serializer(request::class.createType())
78+
}
79+
6780
private fun <T : Any> responseSerializer(resultType: KClass<T>): KSerializer<KotlinxGraphQLResponse<Any?>> =
68-
serializerCache.computeIfAbsent(resultType) {
69-
val resultTypeSerializer = serializer(resultType.createType())
81+
responseSerializerCache.computeIfAbsent(resultType) {
82+
val resultTypeSerializer = json.serializersModule.serializer(resultType.createType())
7083
KotlinxGraphQLResponse.serializer(
7184
resultTypeSerializer
7285
)

0 commit comments

Comments
 (0)