Skip to content

Commit

Permalink
[client] support non JSON primitive scalars (#1488)
Browse files Browse the repository at this point in the history
Update `kotlinx-serialization` handling of custom scalars to allow non-primitive values. While custom scalars are most often represented as some primitive value (e.g. UUID -> String), there are use cases when scalars need to be represented as objects. One common use case is Apollo Federation and its usage of `_Any` scalar to represent arbitrary entity object map that contains various fields to uniquely identity it.

Updated custom scalar documentation with examples  n how to use arbitrary objects as custom scalars.

Resolves:

* #1408
* #1435
* #1445
  • Loading branch information
dariuszkuc authored Jul 26, 2022
1 parent a911ded commit 186c46f
Show file tree
Hide file tree
Showing 41 changed files with 1,323 additions and 434 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 @@ -17,6 +17,7 @@
package com.expediagroup.graphql.client.jackson

import com.expediagroup.graphql.client.jackson.data.EmptyInputQuery
import com.expediagroup.graphql.client.jackson.data.EntitiesQuery
import com.expediagroup.graphql.client.jackson.data.EnumQuery
import com.expediagroup.graphql.client.jackson.data.FirstQuery
import com.expediagroup.graphql.client.jackson.data.InputQuery
Expand All @@ -26,6 +27,7 @@ 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.data.scalars.ProductEntityRepresentation
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLError
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLResponse
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLSourceLocation
Expand Down Expand Up @@ -178,7 +180,7 @@ class GraphQLClientJacksonSerializerTest {
@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 scalarQuery = ScalarQuery(variables = ScalarQuery.Variables(alias = "1234", custom = randomUUID))
val expected =
"""{
| "variables" : {
Expand Down Expand Up @@ -208,7 +210,7 @@ class GraphQLClientJacksonSerializerTest {

val result = serializer.deserialize(scalarResponse, ScalarQuery(ScalarQuery.Variables()).responseType())
assertEquals("1234", result.data?.scalarAlias)
assertEquals(expectedUUID, result.data?.customScalar?.value)
assertEquals(expectedUUID, result.data?.customScalar)
}

@Test
Expand Down Expand Up @@ -320,4 +322,24 @@ class GraphQLClientJacksonSerializerTest {
val serialized = serializer.serialize(query)
assertEquals(expected, serialized)
}

@Test
fun `verify we can serialize non-primitive custom scalars`() {
val entitiesQuery = EntitiesQuery(variables = EntitiesQuery.Variables(representations = listOf(ProductEntityRepresentation(id = "apollo-federation"))))
val expected =
"""{
| "variables" : {
| "representations" : [ {
| "id" : "apollo-federation",
| "__typename" : "Product"
| } ]
| },
| "query" : "ENTITIES_QUERY",
| "operationName" : "EntitiesQuery"
|}
""".trimMargin()

val serialized = serializer.serialize(entitiesQuery)
assertEquals(expected, serialized)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.client.jackson.data

import com.expediagroup.graphql.client.jackson.data.entitiesquery._Entity
import com.expediagroup.graphql.client.jackson.data.scalars.AnyToAnyConverter
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import kotlin.reflect.KClass

class EntitiesQuery(
override val variables: Variables,
) : GraphQLClientRequest<EntitiesQuery.Result> {
override val query: String = "ENTITIES_QUERY"

override val operationName: String = "EntitiesQuery"

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

data class Variables(
@JsonSerialize(contentConverter = AnyToAnyConverter::class)
@JsonDeserialize(contentConverter = AnyToAnyConverter::class)
public val representations: List<Any>,
)

data class Result(
/**
* Union of all types that use the @key directive, including both types native to the schema and
* extended types
*/
val _entities: List<_Entity?>,
)
}
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,8 +16,12 @@

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

import com.expediagroup.graphql.client.jackson.data.scalars.UUID
import com.expediagroup.graphql.client.jackson.data.scalars.AnyToUUIDConverter
import com.expediagroup.graphql.client.jackson.data.scalars.UUIDToAnyConverter
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import java.util.UUID
import kotlin.reflect.KClass

// typealiases would be in separate file
Expand All @@ -34,11 +38,15 @@ class ScalarQuery(

data class Variables(
val alias: ID? = null,
@JsonSerialize(converter = UUIDToAnyConverter::class)
@JsonDeserialize(converter = AnyToUUIDConverter::class)
val custom: UUID? = null
)

data class Result(
val scalarAlias: ID,
@JsonSerialize(converter = UUIDToAnyConverter::class)
@JsonDeserialize(converter = AnyToUUIDConverter::class)
val customScalar: UUID
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.client.jackson.data.entitiesquery

import com.fasterxml.jackson.`annotation`.JsonSubTypes
import com.fasterxml.jackson.`annotation`.JsonTypeInfo
import kotlin.String

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "__typename",
defaultImpl = Default_EntityImplementation::class,
)
@JsonSubTypes(
value = [
JsonSubTypes.Type(value = Product::class, name = "Product")
]
)
interface _Entity

data class Product(
public val name: String
) : _Entity

/**
* Fallback _Entity implementation that will be used when unknown/unhandled type is encountered.
*/
class Default_EntityImplementation() : _Entity
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,26 +17,22 @@
package com.expediagroup.graphql.client.jackson.data.scalars

import com.expediagroup.graphql.client.converter.ScalarConverter
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.databind.util.StdConverter
import kotlin.Any

data class UUID(
val value: java.util.UUID
) {
@JsonValue
fun rawValue() = converter.toJson(value)
class AnyToAnyConverter : StdConverter<Any, Any>() {
private val converter: AnyScalarConverter = AnyScalarConverter()

companion object {
val converter: UUIDScalarConverter = UUIDScalarConverter()

@JsonCreator
@JvmStatic
fun create(rawValue: Any) = UUID(converter.toScalar(rawValue))
}
override fun convert(`value`: Any): Any = converter.toScalar(value)
}

// scalar converter would not be part of the generated sources
class UUIDScalarConverter : ScalarConverter<java.util.UUID> {
override fun toScalar(rawValue: Any): java.util.UUID = java.util.UUID.fromString(rawValue.toString())
override fun toJson(value: java.util.UUID): String = value.toString()
class AnyScalarConverter : ScalarConverter<Any> {
override fun toScalar(rawValue: Any): Any = rawValue
override fun toJson(value: Any): Any = value
}

// representation would not be part of the generated sources
data class ProductEntityRepresentation(val id: String) {
val __typename: String = "Product"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.client.jackson.data.scalars

import com.expediagroup.graphql.client.converter.ScalarConverter
import com.fasterxml.jackson.databind.util.StdConverter
import java.util.UUID
import kotlin.Any

class AnyToUUIDConverter : StdConverter<Any, UUID>() {
private val converter: UUIDScalarConverter = UUIDScalarConverter()

override fun convert(`value`: Any): UUID = converter.toScalar(value)
}

class UUIDToAnyConverter : StdConverter<UUID, Any>() {
private val converter: UUIDScalarConverter = UUIDScalarConverter()

override fun convert(`value`: UUID): Any = converter.toJson(value)
}

// scalar converter would not be part of the generated sources
class UUIDScalarConverter : ScalarConverter<UUID> {
override fun toScalar(rawValue: Any): UUID = UUID.fromString(rawValue.toString())
override fun toJson(value: UUID): String = value.toString()
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.expediagroup.graphql.client.serialization

import com.expediagroup.graphql.client.serialization.data.EmptyInputQuery
import com.expediagroup.graphql.client.serialization.data.EntitiesQuery
import com.expediagroup.graphql.client.serialization.data.EnumQuery
import com.expediagroup.graphql.client.serialization.data.FirstQuery
import com.expediagroup.graphql.client.serialization.data.InputQuery
Expand All @@ -30,6 +31,9 @@ import com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLError
import com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLResponse
import com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLSourceLocation
import com.expediagroup.graphql.client.serialization.types.OptionalInput
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import org.junit.jupiter.api.Test
import java.util.UUID
import kotlin.test.assertEquals
Expand Down Expand Up @@ -332,4 +336,34 @@ class GraphQLClientKotlinXSerializerTest {
val serialized = serializer.serialize(query)
assertEquals(expected, serialized)
}

@Test
fun `verify we can serialize non-primitive custom scalars`() {
val entity = Json.decodeFromString<JsonObject>(
"""
|{
| "__typename": "Product",
| "id": "apollo-federation"
|}
""".trimMargin()
)
val entitiesQuery = EntitiesQuery(variables = EntitiesQuery.Variables(representations = listOf(entity)))
val expected =
"""{
| "variables": {
| "representations": [
| {
| "__typename": "Product",
| "id": "apollo-federation"
| }
| ]
| },
| "query": "ENTITIES_QUERY",
| "operationName": "EntitiesQuery"
|}
""".trimMargin()

val serialized = serializer.serialize(entitiesQuery)
assertEquals(expected, serialized)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.client.serialization.data

import com.expediagroup.graphql.client.Generated
import com.expediagroup.graphql.client.serialization.data.scalars.JsonObjectSerializer
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import com.expediagroup.graphql.client.serialization.data.entitiesquery._Entity
import kotlin.String
import kotlin.collections.List
import kotlin.reflect.KClass
import kotlinx.serialization.Required
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject

@Serializable
class EntitiesQuery(
override val variables: Variables,
) : GraphQLClientRequest<EntitiesQuery.Result> {
@Required
override val query: String = "ENTITIES_QUERY"

@Required
override val operationName: String = "EntitiesQuery"

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

@Generated
@Serializable
public data class Variables(
public val representations: List<@Serializable(with = JsonObjectSerializer::class) JsonObject>,
)

@Generated
@Serializable
public data class Result(
/**
* Union of all types that use the @key directive, including both types native to the schema and
* extended types
*/
public val _entities: List<_Entity?>,
)
}
Loading

0 comments on commit 186c46f

Please sign in to comment.