diff --git a/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/GraphQLClientJacksonSerializerTest.kt b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/GraphQLClientJacksonSerializerTest.kt index 905d73355c..ed3a1d8341 100644 --- a/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/GraphQLClientJacksonSerializerTest.kt +++ b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/GraphQLClientJacksonSerializerTest.kt @@ -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. @@ -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 @@ -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 @@ -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" : { @@ -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 @@ -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) + } } diff --git a/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/EntitiesQuery.kt b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/EntitiesQuery.kt new file mode 100644 index 0000000000..7bcce55dd5 --- /dev/null +++ b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/EntitiesQuery.kt @@ -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 { + override val query: String = "ENTITIES_QUERY" + + override val operationName: String = "EntitiesQuery" + + override fun responseType(): KClass = Result::class + + data class Variables( + @JsonSerialize(contentConverter = AnyToAnyConverter::class) + @JsonDeserialize(contentConverter = AnyToAnyConverter::class) + public val representations: List, + ) + + 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?>, + ) +} diff --git a/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/ScalarQuery.kt b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/ScalarQuery.kt index 4e77851943..3eab5f7897 100644 --- a/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/ScalarQuery.kt +++ b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/ScalarQuery.kt @@ -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. @@ -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 @@ -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 ) } diff --git a/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/entitiesquery/_Entity.kt b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/entitiesquery/_Entity.kt new file mode 100644 index 0000000000..16ea2ae041 --- /dev/null +++ b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/entitiesquery/_Entity.kt @@ -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 diff --git a/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/scalars/UUID.kt b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/scalars/AnyToAnyConverter.kt similarity index 52% rename from clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/scalars/UUID.kt rename to clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/scalars/AnyToAnyConverter.kt index 37570be8ef..fa0099f07a 100644 --- a/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/scalars/UUID.kt +++ b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/scalars/AnyToAnyConverter.kt @@ -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. @@ -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() { + 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 { - 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 { + 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" } diff --git a/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/scalars/UUIDConverters.kt b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/scalars/UUIDConverters.kt new file mode 100644 index 0000000000..07dcc782bb --- /dev/null +++ b/clients/graphql-kotlin-client-jackson/src/test/kotlin/com/expediagroup/graphql/client/jackson/data/scalars/UUIDConverters.kt @@ -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() { + private val converter: UUIDScalarConverter = UUIDScalarConverter() + + override fun convert(`value`: Any): UUID = converter.toScalar(value) +} + +class UUIDToAnyConverter : StdConverter() { + 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 { + override fun toScalar(rawValue: Any): UUID = UUID.fromString(rawValue.toString()) + override fun toJson(value: UUID): String = value.toString() +} diff --git a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/GraphQLClientKotlinXSerializerTest.kt b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/GraphQLClientKotlinXSerializerTest.kt index b110df37a3..bf585e44a1 100644 --- a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/GraphQLClientKotlinXSerializerTest.kt +++ b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/GraphQLClientKotlinXSerializerTest.kt @@ -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 @@ -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 @@ -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( + """ + |{ + | "__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) + } } diff --git a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/EntitiesQuery.kt b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/EntitiesQuery.kt new file mode 100644 index 0000000000..2796858223 --- /dev/null +++ b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/EntitiesQuery.kt @@ -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 { + @Required + override val query: String = "ENTITIES_QUERY" + + @Required + override val operationName: String = "EntitiesQuery" + + override fun responseType(): KClass = 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?>, + ) +} diff --git a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/OptionalInputQuery.kt b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/OptionalInputQuery.kt index e1d09b0ee0..482923593e 100644 --- a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/OptionalInputQuery.kt +++ b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/OptionalInputQuery.kt @@ -16,8 +16,10 @@ package com.expediagroup.graphql.client.serialization.data -import com.expediagroup.graphql.client.serialization.data.scalars.OptionalInputSerializer +import com.expediagroup.graphql.client.serialization.data.scalars.OptionalUUIDListSerializer +import com.expediagroup.graphql.client.serialization.data.scalars.OptionalUUIDSerializer import com.expediagroup.graphql.client.serialization.data.scalars.UUIDSerializer +import com.expediagroup.graphql.client.serialization.serializers.OptionalScalarSerializer import com.expediagroup.graphql.client.serialization.types.OptionalInput import com.expediagroup.graphql.client.types.GraphQLClientRequest import kotlinx.serialization.Required @@ -40,15 +42,15 @@ class OptionalInputQuery( @Serializable data class Variables( val requiredInput: Int, - @Serializable(with = OptionalInputSerializer::class) + @Serializable(with = OptionalScalarSerializer::class) val optionalIntInput: OptionalInput = OptionalInput.Undefined, - @Serializable(with = OptionalInputSerializer::class) + @Serializable(with = OptionalScalarSerializer::class) val optionalStringInput: OptionalInput = OptionalInput.Undefined, - @Serializable(with = OptionalInputSerializer::class) + @Serializable(with = OptionalScalarSerializer::class) val optionalBooleanInput: OptionalInput = OptionalInput.Undefined, - @Serializable(with = OptionalInputSerializer::class) + @Serializable(with = OptionalUUIDSerializer::class) val optionalUUIDInput: OptionalInput<@Serializable(with = UUIDSerializer::class) UUID> = OptionalInput.Undefined, - @Serializable(with = OptionalInputSerializer::class) + @Serializable(with = OptionalUUIDListSerializer::class) val optionalUUIDListInput: OptionalInput> = OptionalInput.Undefined, ) diff --git a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/entitiesquery/_Entity.kt b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/entitiesquery/_Entity.kt new file mode 100644 index 0000000000..ab8a72984d --- /dev/null +++ b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/entitiesquery/_Entity.kt @@ -0,0 +1,39 @@ +/* + * 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.entitiesquery + +import kotlin.String +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed class _Entity + +@Serializable +@SerialName(value = "Product") +data class Product( + val name: String +) : _Entity() + +/** + * Fallback _Entity implementation that will be used when unknown/unhandled type is encountered. + * + * NOTE: This fallback logic has to be manually registered with the instance of + * GraphQLClientKotlinxSerializer. See documentation for details. + */ +@Serializable +public class Default_EntityImplementation() : _Entity() diff --git a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/JsonObjectSerializer.kt b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/JsonObjectSerializer.kt new file mode 100644 index 0000000000..69cc7c5d5f --- /dev/null +++ b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/JsonObjectSerializer.kt @@ -0,0 +1,62 @@ +/* + * 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.scalars + +import com.expediagroup.graphql.client.converter.ScalarConverter +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.serializerOrNull + +object JsonObjectSerializer : KSerializer { + private val converter: AnyScalarConverter = AnyScalarConverter() + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("JsonObject") + + override fun serialize(encoder: Encoder, `value`: JsonObject) { + val encoded = converter.toJson(value) + val serializer = serializerOrNull(encoded::class.java) + if (serializer != null) { + encoder.encodeSerializableValue(serializer, encoded) + } else { + encoder.encodeString(encoded.toString()) + } + } + + override fun deserialize(decoder: Decoder): JsonObject { + val jsonDecoder = decoder as JsonDecoder + val rawContent: Any = when (val element = jsonDecoder.decodeJsonElement()) { + is JsonPrimitive -> element.jsonPrimitive.content + else -> element + } + return converter.toScalar(rawContent) + } +} + +// scalar converter would not be part of the generated sources +class AnyScalarConverter : ScalarConverter { + override fun toScalar(rawValue: Any): JsonObject = Json.parseToJsonElement(rawValue.toString()).jsonObject + override fun toJson(value: JsonObject): Any = value +} diff --git a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/OptionalInputSerializer.kt b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/OptionalInputSerializer.kt deleted file mode 100644 index 3225df86aa..0000000000 --- a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/OptionalInputSerializer.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.scalars - -import com.expediagroup.graphql.client.Generated -import com.expediagroup.graphql.client.serialization.serializers.AnyKSerializer -import com.expediagroup.graphql.client.serialization.types.OptionalInput -import java.util.UUID -import kotlin.Any -import kotlin.Suppress -import kotlin.collections.Map -import kotlin.collections.mapOf -import kotlin.reflect.KClass -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -@Generated -object OptionalInputSerializer : KSerializer> { - private val delegates: Map, KSerializer<*>> = mapOf(UUID::class to UUIDSerializer) - - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("OptionalInput") - - @Suppress("UNCHECKED_CAST") - override fun serialize(encoder: Encoder, `value`: OptionalInput) { - when (value) { - is OptionalInput.Undefined -> { - return - } - is OptionalInput.Defined<*> -> { - val definedValue = value.value - if (definedValue != null) { - if (definedValue is List<*>) { - val element = definedValue.firstOrNull() - val elementSerializer = if (element == null) { - AnyKSerializer - } else { - delegates[element::class] as? KSerializer ?: AnyKSerializer - } - encoder.encodeSerializableValue(ListSerializer(elementSerializer), definedValue) - } else { - val delegate: KSerializer = delegates[definedValue::class] as? KSerializer ?: AnyKSerializer - encoder.encodeSerializableValue(delegate, definedValue) - } - } else { - encoder.encodeNull() - } - } - } - } - - /** - * undefined is only supported during client serialization, this code should never be invoked - */ - override fun deserialize(decoder: Decoder): OptionalInput = - OptionalInput.Defined(AnyKSerializer.deserialize(decoder)) -} diff --git a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/OptionalUUIDListSerializer.kt b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/OptionalUUIDListSerializer.kt new file mode 100644 index 0000000000..eeac159c4b --- /dev/null +++ b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/OptionalUUIDListSerializer.kt @@ -0,0 +1,49 @@ +/* + * 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.scalars + +import com.expediagroup.graphql.client.serialization.types.OptionalInput +import java.util.UUID +import kotlin.collections.List +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object OptionalUUIDListSerializer : KSerializer>> { + private val `delegate`: KSerializer> = ListSerializer(UUIDSerializer) + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("OptionalUUIDListSerializer") + + override fun serialize(encoder: Encoder, `value`: OptionalInput>) { + when (value) { + is OptionalInput.Undefined -> return + is OptionalInput.Defined> -> + encoder.encodeNullableSerializableValue(delegate, value.value) + } + } + + /** + * undefined is only supported during client serialization, this code should never be invoked + */ + override fun deserialize(decoder: Decoder): OptionalInput> = + OptionalInput.Defined(decoder.decodeNullableSerializableValue(delegate.nullable)) +} diff --git a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/OptionalUUIDSerializer.kt b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/OptionalUUIDSerializer.kt new file mode 100644 index 0000000000..5a241ea06b --- /dev/null +++ b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/OptionalUUIDSerializer.kt @@ -0,0 +1,47 @@ +/* + * 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.scalars + +import com.expediagroup.graphql.client.serialization.types.OptionalInput +import java.util.UUID +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object OptionalUUIDSerializer : KSerializer> { + private val `delegate`: KSerializer = UUIDSerializer + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("OptionalUUIDSerializer") + + override fun serialize(encoder: Encoder, `value`: OptionalInput) { + when (value) { + is OptionalInput.Undefined -> return + is OptionalInput.Defined -> + encoder.encodeNullableSerializableValue(delegate, value.value) + } + } + + /** + * undefined is only supported during client serialization, this code should never be invoked + */ + override fun deserialize(decoder: Decoder): OptionalInput = + OptionalInput.Defined(decoder.decodeNullableSerializableValue(delegate.nullable)) +} diff --git a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/UUIDSerializer.kt b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/UUIDSerializer.kt index f6b528f68a..7c1e0dc734 100644 --- a/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/UUIDSerializer.kt +++ b/clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/UUIDSerializer.kt @@ -16,39 +16,45 @@ package com.expediagroup.graphql.client.serialization.data.scalars -import com.expediagroup.graphql.client.Generated import com.expediagroup.graphql.client.converter.ScalarConverter import java.util.UUID import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind.STRING -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.serializerOrNull -@Generated object UUIDSerializer : KSerializer { private val converter: UUIDScalarConverter = UUIDScalarConverter() - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", STRING) + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UUID") override fun serialize(encoder: Encoder, `value`: UUID) { val encoded = converter.toJson(value) - encoder.encodeString(encoded.toString()) + val serializer = serializerOrNull(encoded::class.java) + if (serializer != null) { + encoder.encodeSerializableValue(serializer, encoded) + } else { + encoder.encodeString(encoded) + } } override fun deserialize(decoder: Decoder): UUID { val jsonDecoder = decoder as JsonDecoder - val element = jsonDecoder.decodeJsonElement() - val rawContent = element.jsonPrimitive.content + val rawContent: Any = when (val element = jsonDecoder.decodeJsonElement()) { + is JsonPrimitive -> element.jsonPrimitive.content + else -> element + } return converter.toScalar(rawContent) } } // scalar converter would not be part of the generated sources -class UUIDScalarConverter : ScalarConverter { - 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 UUIDScalarConverter : ScalarConverter { + override fun toScalar(rawValue: Any): UUID = UUID.fromString(rawValue.toString()) + override fun toJson(value: UUID): String = value.toString() } diff --git a/examples/client/gradle-client/README.md b/examples/client/gradle-client/README.md index 095538c9b8..eb9598d995 100644 --- a/examples/client/gradle-client/README.md +++ b/examples/client/gradle-client/README.md @@ -16,6 +16,7 @@ gradle clean build ## Running locally +* Start server in `server-client-example`, see project README for details * **[only works after project is build]** Run `Application.kt` directly from your IDE * Alternatively you can also use the Gradle application plugin by running `gradle run` from the command line diff --git a/examples/client/gradle-client/build.gradle.kts b/examples/client/gradle-client/build.gradle.kts index 46b8b0c88e..89abbdc289 100644 --- a/examples/client/gradle-client/build.gradle.kts +++ b/examples/client/gradle-client/build.gradle.kts @@ -32,8 +32,9 @@ graphql { allowDeprecatedFields = true headers = mapOf("X-Custom-Header" to "My-Custom-Header") customScalars = listOf( - GraphQLScalar("UUID", "java.util.UUID", "com.expediagroup.graphql.examples.client.gradle.UUIDScalarConverter"), + GraphQLScalar("_Any", "kotlinx.serialization.json.JsonObject", "com.expediagroup.graphql.examples.client.gradle.AnyScalarConverter"), GraphQLScalar("Locale", "com.ibm.icu.util.ULocale", "com.expediagroup.graphql.examples.client.gradle.ULocaleScalarConverter"), + GraphQLScalar("UUID", "java.util.UUID", "com.expediagroup.graphql.examples.client.gradle.UUIDScalarConverter"), ) serializer = GraphQLSerializer.KOTLINX } diff --git a/examples/client/gradle-client/src/main/kotlin/com/expediagroup/graphql/examples/client/gradle/AnyScalarConverter.kt b/examples/client/gradle-client/src/main/kotlin/com/expediagroup/graphql/examples/client/gradle/AnyScalarConverter.kt new file mode 100644 index 0000000000..3e7db55fb5 --- /dev/null +++ b/examples/client/gradle-client/src/main/kotlin/com/expediagroup/graphql/examples/client/gradle/AnyScalarConverter.kt @@ -0,0 +1,27 @@ +/* + * 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.examples.client.gradle + +import com.expediagroup.graphql.client.converter.ScalarConverter +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject + +class AnyScalarConverter : ScalarConverter { + override fun toScalar(rawValue: Any): JsonObject = Json.parseToJsonElement(rawValue.toString()).jsonObject + override fun toJson(value: JsonObject): Any = value +} diff --git a/examples/client/gradle-client/src/main/kotlin/com/expediagroup/graphql/examples/client/gradle/Application.kt b/examples/client/gradle-client/src/main/kotlin/com/expediagroup/graphql/examples/client/gradle/KtorClientApplication.kt similarity index 72% rename from examples/client/gradle-client/src/main/kotlin/com/expediagroup/graphql/examples/client/gradle/Application.kt rename to examples/client/gradle-client/src/main/kotlin/com/expediagroup/graphql/examples/client/gradle/KtorClientApplication.kt index fe47459e9c..205d0f657d 100644 --- a/examples/client/gradle-client/src/main/kotlin/com/expediagroup/graphql/examples/client/gradle/Application.kt +++ b/examples/client/gradle-client/src/main/kotlin/com/expediagroup/graphql/examples/client/gradle/KtorClientApplication.kt @@ -19,10 +19,12 @@ package com.expediagroup.graphql.examples.client.gradle import com.expediagroup.graphql.client.ktor.GraphQLKtorClient import com.expediagroup.graphql.client.types.GraphQLClientResponse import com.expediagroup.graphql.generated.AddObjectMutation +import com.expediagroup.graphql.generated.EntitiesQuery import com.expediagroup.graphql.generated.ExampleQuery import com.expediagroup.graphql.generated.HelloWorldQuery import com.expediagroup.graphql.generated.RetrieveObjectQuery import com.expediagroup.graphql.generated.UpdateObjectMutation +import com.expediagroup.graphql.generated.entitiesquery.Product import com.expediagroup.graphql.generated.inputs.BasicObjectInput import com.expediagroup.graphql.generated.inputs.SimpleArgumentInput import io.ktor.client.HttpClient @@ -32,10 +34,19 @@ import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import kotlinx.coroutines.runBlocking +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject import java.net.URL import java.util.concurrent.TimeUnit fun main() { + /* + * ************************************************************ + * Make sure to start example server before running this code + * https://github.com/dariuszkuc/graphql-kotlin/blob/client-custom-scalars/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/Application.kt + * ************************************************************ + */ val httpClient = HttpClient(engineFactory = OkHttp) { engine { config { @@ -57,6 +68,8 @@ fun main() { runBlocking { val helloWorldQuery = HelloWorldQuery(variables = HelloWorldQuery.Variables()) val helloWorldResult = client.execute(helloWorldQuery) + println("\tquery without parameters result: ${helloWorldResult.data?.helloWorld}") + val helloWorldResultImplicit: GraphQLClientResponse = client.execute(helloWorldQuery) val results = client.execute( @@ -68,7 +81,7 @@ fun main() { val resultsNoParam = results[0].data as? HelloWorldQuery.Result val resultsWithParam = results[1].data as? HelloWorldQuery.Result - println("\tquery without parameters result: ${resultsNoParam?.helloWorld}") + println("\tquery with null name result: ${resultsNoParam?.helloWorld}") println("\tquery with parameters result: ${resultsWithParam?.helloWorld}") } @@ -95,5 +108,23 @@ fun main() { println("\tretrieved example list: [${exampleData.data?.listQuery?.joinToString { it.name }}]") } + println("entities query") + runBlocking { + val entity = Json.decodeFromString( + """ + |{ + | "__typename": "Product", + | "id": "apollo-federation" + |} + """.trimMargin() + ) + val entityData = client.execute(EntitiesQuery(variables = EntitiesQuery.Variables(representations = listOf(entity)))) + val product = entityData.data?._entities?.get(0) as? Product + println("\tretrieved product SKU: ${product?.sku}") + println("\tretrieved product package: ${product?.`package`}") + println("\tretrieved product variation ID: ${product?.variation?.id}") + println("\tretrieved product dimensions: size=${product?.dimensions?.size}, weight=${product?.dimensions?.weight}") + } + client.close() } diff --git a/examples/client/gradle-client/src/main/resources/EntitiesQuery.graphql b/examples/client/gradle-client/src/main/resources/EntitiesQuery.graphql new file mode 100644 index 0000000000..e7d7ae0615 --- /dev/null +++ b/examples/client/gradle-client/src/main/resources/EntitiesQuery.graphql @@ -0,0 +1,7 @@ +query EntitiesQuery($representations: [_Any!]!) { + _entities(representations: $representations) { + __typename + ...on Product {sku package variation { id } dimensions { size weight } + } + } +} diff --git a/examples/client/maven-client/README.md b/examples/client/maven-client/README.md index d1024bcaae..13d2f03857 100644 --- a/examples/client/maven-client/README.md +++ b/examples/client/maven-client/README.md @@ -24,6 +24,7 @@ you can build it as ## Running locally +* Start server in `server-client-example`, see project README for details * **[only works after project is build]** Run `Application.kt` directly from your IDE * Alternatively you can also use the Maven exec plugin by running `./mvnw exec:java` from the command line diff --git a/examples/client/maven-client/pom.xml b/examples/client/maven-client/pom.xml index 227ced788f..dc62490e37 100755 --- a/examples/client/maven-client/pom.xml +++ b/examples/client/maven-client/pom.xml @@ -85,14 +85,15 @@ com.expediagroup.graphql.examples.client.maven.UUIDScalarConverter - Locale - com.ibm.icu.util.ULocale - com.expediagroup.graphql.examples.client.maven.ULocaleScalarConverter + + _Any + kotlin.Any + com.expediagroup.graphql.examples.client.maven.AnyScalarConverter + JACKSON true diff --git a/examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/AnyScalarConverter.kt b/examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/AnyScalarConverter.kt new file mode 100644 index 0000000000..0412c3e016 --- /dev/null +++ b/examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/AnyScalarConverter.kt @@ -0,0 +1,27 @@ +/* + * 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.examples.client.maven + +import com.expediagroup.graphql.client.converter.ScalarConverter + +/** + * Simple pass-through converter to allow specifying arbitrary objects for custom scalars. + */ +class AnyScalarConverter : ScalarConverter { + override fun toScalar(rawValue: Any): Any = rawValue + override fun toJson(value: Any): Any = value +} diff --git a/examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/ProductEntityRepresentation.kt b/examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/ProductEntityRepresentation.kt new file mode 100644 index 0000000000..a31cca20e8 --- /dev/null +++ b/examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/ProductEntityRepresentation.kt @@ -0,0 +1,23 @@ +/* + * 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.examples.client.maven + +/** + * Representation of a Product type entity. + */ +data class ProductEntityRepresentation(val id: String) { + val __typename: String = "Product" +} diff --git a/examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/Application.kt b/examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/SpringWebclientApplication.kt similarity index 77% rename from examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/Application.kt rename to examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/SpringWebclientApplication.kt index da44d29295..3c1d8d4694 100644 --- a/examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/Application.kt +++ b/examples/client/maven-client/src/main/kotlin/com/expediagroup/graphql/examples/client/maven/SpringWebclientApplication.kt @@ -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. @@ -19,10 +19,12 @@ package com.expediagroup.graphql.examples.client.maven import com.expediagroup.graphql.client.jackson.types.OptionalInput import com.expediagroup.graphql.client.spring.GraphQLWebClient import com.expediagroup.graphql.generated.AddObjectMutation +import com.expediagroup.graphql.generated.EntitiesQuery import com.expediagroup.graphql.generated.ExampleQuery import com.expediagroup.graphql.generated.HelloWorldQuery import com.expediagroup.graphql.generated.RetrieveObjectQuery import com.expediagroup.graphql.generated.UpdateObjectMutation +import com.expediagroup.graphql.generated.entitiesquery.Product import com.expediagroup.graphql.generated.inputs.BasicObjectInput import com.expediagroup.graphql.generated.inputs.SimpleArgumentInput import io.netty.channel.ChannelOption @@ -34,6 +36,12 @@ import reactor.netty.http.client.HttpClient import java.time.Duration fun main() { + /* + * ************************************************************ + * Make sure to start example server before running this code + * https://github.com/dariuszkuc/graphql-kotlin/blob/client-custom-scalars/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/Application.kt + * ************************************************************ + */ val httpClient: HttpClient = HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000) .responseTimeout(Duration.ofMillis(60_000)) @@ -82,4 +90,14 @@ fun main() { println("\tretrieved enum: ${exampleData.data?.enumQuery} ") println("\tretrieved example list: [${exampleData.data?.listQuery?.joinToString { it.name }}]") } + + println("entities query") + runBlocking { + val entityData = client.execute(EntitiesQuery(variables = EntitiesQuery.Variables(representations = listOf(ProductEntityRepresentation(id = "apollo-federation"))))) + val product = entityData.data?._entities?.get(0) as? Product + println("\tretrieved product SKU: ${product?.sku}") + println("\tretrieved product package: ${product?.`package`}") + println("\tretrieved product variation ID: ${product?.variation?.id}") + println("\tretrieved product dimensions: size=${product?.dimensions?.size}, weight=${product?.dimensions?.weight}") + } } diff --git a/examples/client/maven-client/src/main/resources/EntitiesQuery.graphql b/examples/client/maven-client/src/main/resources/EntitiesQuery.graphql new file mode 100644 index 0000000000..e7d7ae0615 --- /dev/null +++ b/examples/client/maven-client/src/main/resources/EntitiesQuery.graphql @@ -0,0 +1,7 @@ +query EntitiesQuery($representations: [_Any!]!) { + _entities(representations: $representations) { + __typename + ...on Product {sku package variation { id } dimensions { size weight } + } + } +} diff --git a/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/Application.kt b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/Application.kt index ae63ab4970..3cd53ab6d9 100644 --- a/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/Application.kt +++ b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/Application.kt @@ -16,29 +16,11 @@ package com.expediagroup.graphql.examples.client.server -import com.expediagroup.graphql.examples.client.server.scalars.graphqlULocaleType -import com.expediagroup.graphql.examples.client.server.scalars.graphqlUUIDType -import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks -import com.ibm.icu.util.ULocale -import graphql.schema.GraphQLType import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication -import org.springframework.context.annotation.Bean -import java.util.UUID -import kotlin.reflect.KType @SpringBootApplication -class Application { - - @Bean - fun customHooks(): SchemaGeneratorHooks = object : SchemaGeneratorHooks { - override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier) { - UUID::class -> graphqlUUIDType - ULocale::class -> graphqlULocaleType - else -> null - } - } -} +class Application fun main(args: Array) { runApplication(*args) diff --git a/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/CustomFederatedHooks.kt b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/CustomFederatedHooks.kt new file mode 100644 index 0000000000..f1e47286cb --- /dev/null +++ b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/CustomFederatedHooks.kt @@ -0,0 +1,20 @@ +package com.expediagroup.graphql.examples.client.server + +import com.expediagroup.graphql.examples.client.server.scalars.graphqlULocaleType +import com.expediagroup.graphql.examples.client.server.scalars.graphqlUUIDType +import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorHooks +import com.expediagroup.graphql.generator.federation.execution.FederatedTypeResolver +import com.ibm.icu.util.ULocale +import graphql.schema.GraphQLType +import org.springframework.stereotype.Component +import java.util.UUID +import kotlin.reflect.KType + +@Component +class CustomFederatedHooks(resolvers: List>) : FederatedSchemaGeneratorHooks(resolvers, true) { + override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier) { + UUID::class -> graphqlUUIDType + ULocale::class -> graphqlULocaleType + else -> super.willGenerateGraphQLType(type) + } +} diff --git a/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/FederatedQuery.kt b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/FederatedQuery.kt new file mode 100644 index 0000000000..c4217cb2a4 --- /dev/null +++ b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/FederatedQuery.kt @@ -0,0 +1,132 @@ +package com.expediagroup.graphql.examples.client.server + +import com.expediagroup.graphql.generator.annotations.GraphQLName +import com.expediagroup.graphql.generator.federation.directives.ExtendsDirective +import com.expediagroup.graphql.generator.federation.directives.ExternalDirective +import com.expediagroup.graphql.generator.federation.directives.FieldSet +import com.expediagroup.graphql.generator.federation.directives.InaccessibleDirective +import com.expediagroup.graphql.generator.federation.directives.KeyDirective +import com.expediagroup.graphql.generator.federation.directives.OverrideDirective +import com.expediagroup.graphql.generator.federation.directives.ProvidesDirective +import com.expediagroup.graphql.generator.federation.directives.ShareableDirective +import com.expediagroup.graphql.generator.federation.directives.TagDirective +import com.expediagroup.graphql.generator.federation.execution.FederatedTypeResolver +import com.expediagroup.graphql.generator.scalars.ID +import com.expediagroup.graphql.server.operations.Query +import graphql.schema.DataFetchingEnvironment +import org.springframework.stereotype.Component + +@Component +class FederatedQuery : Query { + + fun product(id: ID) = Product.byID(id) +} + +@KeyDirective(fields = FieldSet("id")) +// @KeyDirective(fields = FieldSet("sku package")) +// @KeyDirective(fields = FieldSet("sku variation { id }")) +data class Product( + val id: ID, + val sku: String? = null, + @GraphQLName("package") + val pkg: String? = null, + val variation: ProductVariation? = null, + val dimensions: ProductDimension? = null, + @ProvidesDirective(FieldSet("totalProductsCreated")) + val createdBy: User? = null, + @TagDirective("internal") + val notes: String? = null +) { + companion object { + fun byID(id: ID) = PRODUCTS.find { it.id.value == id.value } + private fun bySkuAndPackage(sku: String, pkg: String) = PRODUCTS.find { it.sku == sku && it.pkg == pkg } + private fun bySkuAndVariation(sku: String, variationId: String) = + PRODUCTS.find { it.sku == sku && it.variation?.id?.value == variationId } + + fun byReference(ref: Map): Product? { + val id = ref["id"]?.toString() + val sku = ref["sku"]?.toString() + val pkg = ref["package"]?.toString() + val variation = ref["variation"] + val variationId = if (variation is Map<*, *>) { + variation["id"].toString() + } else null + + return when { + id != null -> byID(ID(id)) + sku != null && pkg != null -> bySkuAndPackage(sku, pkg) + sku != null && variationId != null -> bySkuAndVariation(sku, variationId) + else -> throw RuntimeException("invalid entity reference") + } + } + } +} + +val PRODUCTS = listOf( + Product( + ID("apollo-federation"), + "federation", + "@apollo/federation", + ProductVariation(ID("OSS")), + ProductDimension("small", 1.0f), + User(email = "support@apollographql.com", name = "support", totalProductsCreated = 1337) + ), + Product( + ID("apollo-studio"), + "studio", + "", + ProductVariation(ID("platform")), + ProductDimension("small", 1.0f), + User(email = "support@apollographql.com", name = "support", totalProductsCreated = 1337) + ) +) + +@ShareableDirective +data class ProductDimension( + val size: String? = null, + val weight: Float? = null, + @InaccessibleDirective + val unit: String? = null +) + +data class ProductVariation( + val id: ID +) + +@KeyDirective(fields = FieldSet("email")) +@ExtendsDirective +data class User( + @ExternalDirective + val email: String, + @OverrideDirective(from = "users") + val name: String, + @ExternalDirective + val totalProductsCreated: Int? = null +) + +@Component +class ProductsResolver : FederatedTypeResolver { + override val typeName: String = "Product" + + override suspend fun resolve( + environment: DataFetchingEnvironment, + representations: List> + ): List = representations.map { + Product.byReference(it) + } +} + +@Component +class UserResolver : FederatedTypeResolver { + override val typeName: String = "User" + + override suspend fun resolve( + environment: DataFetchingEnvironment, + representations: List> + ): List { + return representations.map { + val email = it["email"]?.toString() ?: throw RuntimeException("invalid entity reference") + User(email = email, name = "default", totalProductsCreated = 1337) + } + } +} diff --git a/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/SimpleQueries.kt b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/SimpleQueries.kt index 35b68cc6d9..3c47c4acd9 100755 --- a/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/SimpleQueries.kt +++ b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/SimpleQueries.kt @@ -41,7 +41,7 @@ class SimpleQueries(private val repository: BasicObjectRepository) : Query { private val random = Random @GraphQLDescription("Basic `HelloWorld` Query") - fun helloWorld(@GraphQLDescription("optional name, defaults to `World` if not specified") name: String?) = + fun helloWorld(@GraphQLDescription("optional name, defaults to `World` if not specified") name: String? = null) = "Hello ${name ?: "World"}" @GraphQLDescription("Query that returns enum value") @@ -128,7 +128,7 @@ class SimpleQueries(private val repository: BasicObjectRepository) : Query { } @GraphQLDescription("Query that accepts some input arguments") - fun inputObjectQuery(criteria: SimpleArgument?): Boolean = random.nextBoolean() + fun inputObjectQuery(criteria: SimpleArgument? = null): Boolean = random.nextBoolean() @Deprecated(message = "old query should not be used") @GraphQLDescription("Deprecated query that should not be used anymore") diff --git a/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/model/SimpleArgument.kt b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/model/SimpleArgument.kt index 7197b0133e..aebaa6edf0 100755 --- a/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/model/SimpleArgument.kt +++ b/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/model/SimpleArgument.kt @@ -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. @@ -21,9 +21,9 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDescription @GraphQLDescription("Test input object") data class SimpleArgument( @GraphQLDescription("New value to be set") - val newName: String?, + val newName: String? = null, @GraphQLDescription("Minimum value for test criteria") - val min: Float?, + val min: Double? = null, @GraphQLDescription("Maximum value for test criteria") - val max: Float? + val max: Double? = null ) diff --git a/examples/client/server/src/main/resources/application.yml b/examples/client/server/src/main/resources/application.yml index c36626c9c8..60b1e84fea 100644 --- a/examples/client/server/src/main/resources/application.yml +++ b/examples/client/server/src/main/resources/application.yml @@ -1,2 +1,5 @@ graphql: packages: "com.expediagroup.graphql.examples" + federation: + enabled: true + optInV2: true diff --git a/examples/client/src/integration/wiremock/__files/schema.graphql b/examples/client/src/integration/wiremock/__files/schema.graphql index 92162c33af..07fa7592c3 100644 --- a/examples/client/src/integration/wiremock/__files/schema.graphql +++ b/examples/client/src/integration/wiremock/__files/schema.graphql @@ -1,48 +1,80 @@ -schema { - query: Query - mutation: Mutation +schema @link(url : "https://specs.apollo.dev/link/v1.0/") @link(import : ["extends", "external", "inaccessible", "key", "link", "override", "provides", "requires", "shareable", "tag", "_FieldSet"], url : "https://www.apollographql.com/docs/federation/federation-spec/"){ + query: Query + mutation: Mutation } +"Marks the field, argument, input field or enum value as deprecated" +directive @deprecated( + "The reason for the deprecation" + reason: String = "No longer supported" +) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION + +"Marks target object as extending part of the federated schema" +directive @extends on OBJECT | INTERFACE + +"Marks target field as external meaning it will be resolved by federated schema" +directive @external on FIELD_DEFINITION + +"Marks location within schema as inaccessible from the GraphQL Gateway" +directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + "Directs the executor to include this field or fragment only when the `if` argument is true" directive @include( "Included when true." if: Boolean! - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +"Space separated list of primary keys needed to access federated object" +directive @key(fields: _FieldSet!, resolvable: Boolean) repeatable on OBJECT | INTERFACE + +"Links definitions within the document to external schemas." +directive @link(import: [String], url: String) repeatable on SCHEMA + +"Overrides fields resolution logic from other subgraph. Used for migrating fields from one subgraph to another." +directive @override(from: String!) repeatable on FIELD_DEFINITION + +"Specifies the base type field set that will be selectable by the gateway" +directive @provides(fields: _FieldSet!) on FIELD_DEFINITION + +"Specifies required input field set from the base type for a resolver" +directive @requires(fields: _FieldSet!) on FIELD_DEFINITION + +"Indicates that given object and/or field can be resolved by multiple subgraphs" +directive @shareable on OBJECT | FIELD_DEFINITION "Directs the executor to skip this field or fragment when the `if`'argument is true." directive @skip( "Skipped when true." if: Boolean! - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - -"Marks the field or enum value as deprecated" -directive @deprecated( - "The reason for the deprecation" - reason: String = "No longer supported" - ) on FIELD_DEFINITION | ENUM_VALUE +) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT "Exposes a URL that specifies the behaviour of this scalar." directive @specifiedBy( "The URL that specifies the behaviour of this scalar." url: String! - ) on SCALAR +) on SCALAR + +"Allows users to annotate fields and types with additional metadata information" +directive @tag(name: String!) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION "Very basic interface" interface BasicInterface { - "Unique identifier of an interface" - id: Int! - "Name field" - name: String! + "Unique identifier of an interface" + id: Int! + "Name field" + name: String! } "Very basic union of BasicObject and ComplexObject" union BasicUnion = BasicObject | ComplexObject +union _Entity = Product | User + "Some basic description" type BasicObject { - id: Int! - "Object name" - name: String! + id: Int! + "Object name" + name: String! } """ @@ -51,147 +83,187 @@ This is a second line of the paragraph. This is final line of the description. """ type ComplexObject { - "Some additional details" - details: DetailsObject! - "Some unique identifier" - id: Int! - "Some object name" - name: String! - """ - Optional value - Second line of the description - """ - optional: String + "Some additional details" + details: DetailsObject! + "Some unique identifier" + id: Int! + "Some object name" + name: String! + """ + Optional value + |Second line of the description + """ + optional: String } "Inner type object description" type DetailsObject { - "Boolean flag" - flag: Boolean! - "Unique identifier" - id: Int! - "Actual detail value" - value: String! + "Boolean flag" + flag: Boolean! + "Unique identifier" + id: Int! + "Actual detail value" + value: String! } "Example interface implementation where value is an integer" type FirstInterfaceImplementation implements BasicInterface { - "Unique identifier of the first implementation" - id: Int! - "Custom field integer value" - intValue: Int! - "Name of the first implementation" - name: String! + "Unique identifier of the first implementation" + id: Int! + "Custom field integer value" + intValue: Int! + "Name of the first implementation" + name: String! } type Mutation { - "Add object to the repository" - addBasicObject(newObject: BasicObjectInput!): BasicObject - "Delete object from repository" - deleteBasicObject(id: Int!): BasicObject - "Update existing object in the repository" - updateBasicObject(updatedObject: BasicObjectInput!): BasicObject + "Add object to the repository" + addBasicObject(newObject: BasicObjectInput!): BasicObject + "Delete object from repository" + deleteBasicObject(id: Int!): BasicObject + "Update existing object in the repository" + updateBasicObject(updatedObject: BasicObjectInput!): BasicObject } "Example of an object self-referencing itself" type NestedObject { - "Children elements" - children: [NestedObject!]! - "Unique identifier" - id: Int! - "Name of the object" - name: String! -} - -type Query { - "Query returning an object that references another object" - complexObjectQuery: ComplexObject! - "Deprecated query that should not be used anymore" - deprecatedQuery: String! @deprecated(reason : "old query should not be used") - "Query that returns enum value" - enumQuery: CustomEnum! - "Basic `HelloWorld` Query" - helloWorld( - "optional name, defaults to `World` if not specified" - name: String - ): String! - "Query that accepts some input arguments" - inputObjectQuery(criteria: SimpleArgumentInput): Boolean! - "Query returning an interface" - interfaceQuery: BasicInterface! - "Query returning list of simple objects" - listQuery: [BasicObject!]! - "Query returning object referencing itself" - nestedObjectQuery: NestedObject! - "Retrieve simple object from the repository" - retrieveBasicObject(id: Int!): BasicObject - "Query that returns wrapper object with all supported scalar types" - scalarQuery: ScalarWrapper! - "Query returning union" - unionQuery: BasicUnion! + "Children elements" + children: [NestedObject!]! + "Unique identifier" + id: Int! + "Name of the object" + name: String! +} + +type Product @key(fields : "id", resolvable : true) { + createdBy: User @provides(fields : "totalProductsCreated") + dimensions: ProductDimension + id: ID! + notes: String @tag(name : "internal") + package: String + sku: String + variation: ProductVariation +} + +type ProductDimension @shareable { + size: String + unit: String @inaccessible + weight: Float +} + +type ProductVariation { + id: ID! +} + +type Query @extends { + "Union of all types that use the @key directive, including both types native to the schema and extended types" + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + "Query returning an object that references another object" + complexObjectQuery: ComplexObject! + "Deprecated query that should not be used anymore" + deprecatedQuery: String! @deprecated(reason : "old query should not be used") + "Query that returns enum value" + enumQuery: CustomEnum! + "Basic `HelloWorld` Query" + helloWorld( + "optional name, defaults to `World` if not specified" + name: String + ): String! + "Query that accepts some input arguments" + inputObjectQuery(criteria: SimpleArgumentInput): Boolean! + "Query returning an interface" + interfaceQuery: BasicInterface! + "Query returning list of simple objects" + listQuery: [BasicObject!]! + "Query returning object referencing itself" + nestedObjectQuery: NestedObject! + product(id: ID!): Product + "Retrieve simple object from the repository" + retrieveBasicObject(id: Int!): BasicObject + "Query that returns wrapper object with all supported scalar types" + scalarQuery: ScalarWrapper! + "Query returning union" + unionQuery: BasicUnion! } "Wrapper that holds all supported scalar types" type ScalarWrapper { - "A signed 32-bit nullable integer value" - count: Int - "Custom scalar" - custom: UUID! - "List of custom scalars" - customList: [UUID!]! - "ID represents unique identifier that is not intended to be human readable" - id: ID! - "UTF-8 character sequence" - name: String! - "A nullable signed double-precision floating-point value" - rating: Float - "Either true or false" - valid: Boolean! - "Custom scalar of Locale" - locale: Locale! - "List of custom scalar Locales" - listLocale: [Locale!]! + "A signed 32-bit nullable integer value" + count: Int + "Custom scalar of UUID" + custom: UUID! + "List of custom scalar UUIDs" + customList: [UUID!]! + "ID represents unique identifier that is not intended to be human readable" + id: ID! + "List of custom scalar Locales" + listLocale: [Locale!]! + "Custom scalar of Locale" + locale: Locale! + "UTF-8 character sequence" + name: String! + "A nullable signed double-precision floating-point value" + rating: Float + "Either true or false" + valid: Boolean! } "Example interface implementation where value is a float" type SecondInterfaceImplementation implements BasicInterface { - "Custom field float value" - floatValue: Float! - "Unique identifier of the second implementation" - id: Int! - "Name of the second implementation" - name: String! + "Custom field float value" + floatValue: Float! + "Unique identifier of the second implementation" + id: Int! + "Name of the second implementation" + name: String! +} + +type User @extends @key(fields : "email", resolvable : true) { + email: String! @external + name: String! @override(from : "users") + totalProductsCreated: Int @external +} + +type _Service { + sdl: String! } "Custom enum description" enum CustomEnum { - "First enum value" - ONE - "Third enum value" - THREE @deprecated(reason : "only goes up to two") - "Second enum value" - TWO + "First enum value" + ONE + "Third enum value" + THREE @deprecated(reason : "only goes up to two") + "Second enum value" + TWO } +"A type representing a Locale such as en_US or fr_FR" +scalar Locale + "Custom scalar representing UUID" scalar UUID -"A type representing a Locale such as en_US or fr_FR" -scalar Locale +"Federation scalar type used to represent any external entities passed to _entities query." +scalar _Any + +"Federation type representing set of fields" +scalar _FieldSet "Some basic description" input BasicObjectInput { - id: Int! - "Object name" - name: String! + id: Int! + "Object name" + name: String! } "Test input object" input SimpleArgumentInput { - "Maximum value for test criteria" - max: Float - "Minimum value for test criteria" - min: Float - "New value to be set" - newName: String + "Maximum value for test criteria" + max: Float + "Minimum value for test criteria" + min: Float + "New value to be set" + newName: String } diff --git a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateGraphQLCustomScalarConverters.kt b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateGraphQLCustomScalarConverters.kt index 49124f146d..c6745c4d7c 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateGraphQLCustomScalarConverters.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateGraphQLCustomScalarConverters.kt @@ -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. @@ -29,13 +29,13 @@ import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeSpec -import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asTypeName import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonPrimitive /** * Generate [ScalarConverterInfo] data class that holds information about generated scalar Jackson converters/or kotlinx-serialization serializer. @@ -121,10 +121,9 @@ private fun generateGraphQLCustomScalarKSerializer( .build() serializerTypeSpec.addProperty(converter) - val primitiveSerialDescriptor = MemberName("kotlinx.serialization.descriptors", "PrimitiveSerialDescriptor") - val stringKind = MemberName(PrimitiveKind::class.asClassName(), "STRING") + val scalarSerialDescriptor = MemberName("kotlinx.serialization.descriptors", "buildClassSerialDescriptor") val descriptor = PropertySpec.builder("descriptor", SerialDescriptor::class) - .initializer("%M(%S, %M)", primitiveSerialDescriptor, customScalarName, stringKind) + .initializer("%M(%S)", scalarSerialDescriptor, customScalarName) .addModifiers(KModifier.OVERRIDE) .build() serializerTypeSpec.addProperty(descriptor) @@ -133,21 +132,38 @@ private fun generateGraphQLCustomScalarKSerializer( .addModifiers(KModifier.OVERRIDE) .addParameter("encoder", Encoder::class) .addParameter("value", scalarClassName) - .addStatement("val encoded = converter.toJson(value)") - .addStatement("encoder.encodeString(encoded.toString())") + .addCode( + """ + |val encoded = converter.toJson(value) + |val serializer = %M(encoded::class.java) + |if (serializer != null) { + | encoder.encodeSerializableValue(serializer, encoded) + |} else { + | encoder.encodeString(encoded.toString()) + |} + """.trimMargin(), + MemberName("kotlinx.serialization", "serializerOrNull") + ) .build() serializerTypeSpec.addFunction(serializeFun) - val jsonDecoder = ClassName("kotlinx.serialization.json", "JsonDecoder") - val jsonPrimitive = ClassName("kotlinx.serialization.json", "jsonPrimitive") val deserializeFun = FunSpec.builder("deserialize") .addModifiers(KModifier.OVERRIDE) .returns(scalarClassName) .addParameter("decoder", Decoder::class) - .addStatement("val jsonDecoder = decoder as %T", jsonDecoder) - .addStatement("val element = jsonDecoder.decodeJsonElement()") - .addStatement("val rawContent = element.%T.content", jsonPrimitive) - .addStatement("return converter.toScalar(rawContent)") + .addCode( + """ + |val jsonDecoder = decoder as %T + |val rawContent: Any = when (val element = jsonDecoder.decodeJsonElement()) { + | is %T -> element.%M.content + | else -> element + |} + |return converter.toScalar(rawContent) + """.trimMargin(), + JsonDecoder::class, + JsonPrimitive::class, + MemberName("kotlinx.serialization.json", "jsonPrimitive") + ) .build() serializerTypeSpec.addFunction(deserializeFun) diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalar_input/scalars/ULocaleSerializer.kt b/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalar_input/scalars/ULocaleSerializer.kt index ad63e4249f..ed0ae4034f 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalar_input/scalars/ULocaleSerializer.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalar_input/scalars/ULocaleSerializer.kt @@ -5,29 +5,37 @@ import com.expediagroup.graphql.plugin.client.generator.ULocaleScalarConverter import com.ibm.icu.util.ULocale import kotlin.Unit import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind.STRING -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.serializerOrNull @Generated public object ULocaleSerializer : KSerializer { private val converter: ULocaleScalarConverter = ULocaleScalarConverter() - public override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ULocale", STRING) + public override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ULocale") public override fun serialize(encoder: Encoder, `value`: ULocale): Unit { val encoded = converter.toJson(value) - encoder.encodeString(encoded.toString()) + val serializer = serializerOrNull(encoded::class.java) + if (serializer != null) { + encoder.encodeSerializableValue(serializer, encoded) + } else { + encoder.encodeString(encoded.toString()) + } } public override fun deserialize(decoder: Decoder): ULocale { val jsonDecoder = decoder as JsonDecoder - val element = jsonDecoder.decodeJsonElement() - val rawContent = element.jsonPrimitive.content + val rawContent: Any = when (val element = jsonDecoder.decodeJsonElement()) { + is JsonPrimitive -> element.jsonPrimitive.content + else -> element + } return converter.toScalar(rawContent) } } diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalar_input/scalars/UUIDSerializer.kt b/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalar_input/scalars/UUIDSerializer.kt index a89d432d11..832753647c 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalar_input/scalars/UUIDSerializer.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalar_input/scalars/UUIDSerializer.kt @@ -5,29 +5,37 @@ import com.expediagroup.graphql.plugin.client.generator.UUIDScalarConverter import java.util.UUID import kotlin.Unit import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind.STRING -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.serializerOrNull @Generated public object UUIDSerializer : KSerializer { private val converter: UUIDScalarConverter = UUIDScalarConverter() - public override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", STRING) + public override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UUID") public override fun serialize(encoder: Encoder, `value`: UUID): Unit { val encoded = converter.toJson(value) - encoder.encodeString(encoded.toString()) + val serializer = serializerOrNull(encoded::class.java) + if (serializer != null) { + encoder.encodeSerializableValue(serializer, encoded) + } else { + encoder.encodeString(encoded.toString()) + } } public override fun deserialize(decoder: Decoder): UUID { val jsonDecoder = decoder as JsonDecoder - val element = jsonDecoder.decodeJsonElement() - val rawContent = element.jsonPrimitive.content + val rawContent: Any = when (val element = jsonDecoder.decodeJsonElement()) { + is JsonPrimitive -> element.jsonPrimitive.content + else -> element + } return converter.toScalar(rawContent) } } diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalars/scalars/ULocaleSerializer.kt b/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalars/scalars/ULocaleSerializer.kt index ad63e4249f..ed0ae4034f 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalars/scalars/ULocaleSerializer.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalars/scalars/ULocaleSerializer.kt @@ -5,29 +5,37 @@ import com.expediagroup.graphql.plugin.client.generator.ULocaleScalarConverter import com.ibm.icu.util.ULocale import kotlin.Unit import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind.STRING -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.serializerOrNull @Generated public object ULocaleSerializer : KSerializer { private val converter: ULocaleScalarConverter = ULocaleScalarConverter() - public override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ULocale", STRING) + public override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ULocale") public override fun serialize(encoder: Encoder, `value`: ULocale): Unit { val encoded = converter.toJson(value) - encoder.encodeString(encoded.toString()) + val serializer = serializerOrNull(encoded::class.java) + if (serializer != null) { + encoder.encodeSerializableValue(serializer, encoded) + } else { + encoder.encodeString(encoded.toString()) + } } public override fun deserialize(decoder: Decoder): ULocale { val jsonDecoder = decoder as JsonDecoder - val element = jsonDecoder.decodeJsonElement() - val rawContent = element.jsonPrimitive.content + val rawContent: Any = when (val element = jsonDecoder.decodeJsonElement()) { + is JsonPrimitive -> element.jsonPrimitive.content + else -> element + } return converter.toScalar(rawContent) } } diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalars/scalars/UUIDSerializer.kt b/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalars/scalars/UUIDSerializer.kt index a89d432d11..832753647c 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalars/scalars/UUIDSerializer.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/custom_scalars/scalars/UUIDSerializer.kt @@ -5,29 +5,37 @@ import com.expediagroup.graphql.plugin.client.generator.UUIDScalarConverter import java.util.UUID import kotlin.Unit import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind.STRING -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.serializerOrNull @Generated public object UUIDSerializer : KSerializer { private val converter: UUIDScalarConverter = UUIDScalarConverter() - public override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", STRING) + public override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UUID") public override fun serialize(encoder: Encoder, `value`: UUID): Unit { val encoded = converter.toJson(value) - encoder.encodeString(encoded.toString()) + val serializer = serializerOrNull(encoded::class.java) + if (serializer != null) { + encoder.encodeSerializableValue(serializer, encoded) + } else { + encoder.encodeString(encoded.toString()) + } } public override fun deserialize(decoder: Decoder): UUID { val jsonDecoder = decoder as JsonDecoder - val element = jsonDecoder.decodeJsonElement() - val rawContent = element.jsonPrimitive.content + val rawContent: Any = when (val element = jsonDecoder.decodeJsonElement()) { + is JsonPrimitive -> element.jsonPrimitive.content + else -> element + } return converter.toScalar(rawContent) } } diff --git a/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/multiple_queries/scalars/UUIDSerializer.kt b/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/multiple_queries/scalars/UUIDSerializer.kt index a89d432d11..832753647c 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/multiple_queries/scalars/UUIDSerializer.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/test/data/kotlinx/multiple_queries/scalars/UUIDSerializer.kt @@ -5,29 +5,37 @@ import com.expediagroup.graphql.plugin.client.generator.UUIDScalarConverter import java.util.UUID import kotlin.Unit import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind.STRING -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.serializerOrNull @Generated public object UUIDSerializer : KSerializer { private val converter: UUIDScalarConverter = UUIDScalarConverter() - public override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", STRING) + public override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UUID") public override fun serialize(encoder: Encoder, `value`: UUID): Unit { val encoded = converter.toJson(value) - encoder.encodeString(encoded.toString()) + val serializer = serializerOrNull(encoded::class.java) + if (serializer != null) { + encoder.encodeSerializableValue(serializer, encoded) + } else { + encoder.encodeString(encoded.toString()) + } } public override fun deserialize(decoder: Decoder): UUID { val jsonDecoder = decoder as JsonDecoder - val element = jsonDecoder.decodeJsonElement() - val rawContent = element.jsonPrimitive.content + val rawContent: Any = when (val element = jsonDecoder.decodeJsonElement()) { + is JsonPrimitive -> element.jsonPrimitive.content + else -> element + } return converter.toScalar(rawContent) } } diff --git a/website/docs/client/client-customization.mdx b/website/docs/client/client-customization.mdx index b517610068..072892f956 100644 --- a/website/docs/client/client-customization.mdx +++ b/website/docs/client/client-customization.mdx @@ -124,124 +124,3 @@ class CustomGraphQLClient(url: URL) : GraphQLKtorClient(url = url) { Build plugins will automatically fail generation of a client if any of the specified query files are referencing deprecated fields. This ensures that your clients have to explicitly opt-in into deprecated usage by specifying `allowDeprecatedFields` configuration option. - -## Custom GraphQL Scalars - -By default, custom GraphQL scalars are serialized and [type-aliased](https://kotlinlang.org/docs/reference/type-aliases.html) -to a String. GraphQL Kotlin plugins also support custom serialization based on provided configuration. - -In order to automatically convert between custom GraphQL `UUID` scalar type and `java.util.UUID`, we first need to create -our custom `ScalarConverter`. - -```kotlin -package com.example.client - -import com.expediagroup.graphql.client.converter.ScalarConverter -import java.util.UUID - -class UUIDScalarConverter : ScalarConverter { - override fun toScalar(rawValue: Any): UUID = UUID.fromString(rawValue.toString()) - override fun toJson(value: UUID): Any = value.toString() -} -``` - -And then configure build plugin by specifying - -- Custom GraphQL scalar name -- Target JVM class name -- Converter that provides logic to map between GraphQL and Kotlin type - -```kotlin -graphql { - packageName = "com.example.generated" - endpoint = "http://localhost:8080/graphql" - customScalars = listOf(GraphQLScalar("UUID", "java.util.UUID", "com.example.UUIDScalarConverter")) -} -``` - -Custom scalar fields will then be automatically converted to a `java.util.UUID` type using appropriate converter/serializer. - - - - - -Following converters will be generated under `com.example.generated.scalars` package. - -```kotlin -@Generated -public class AnyToUUIDConverter : StdConverter() { - private val converter: UUIDScalarConverter = UUIDScalarConverter() - - public override fun convert(`value`: Any): UUID = converter.toScalar(value) -} - -@Generated -public class UUIDToAnyConverter : StdConverter() { - private val converter: UUIDScalarConverter = UUIDScalarConverter() - - public override fun convert(`value`: UUID): Any = converter.toJson(value) -} -``` - -Custom scalars fields will then be annotated with Jackson annotations referencing the above converters. - -```kotlin -@Generated -public data class Result( - @JsonSerialize(converter = UUIDToAnyConverter::class) - @JsonDeserialize(converter = AnyToUUIDConverter::class) - public val custom: UUID, - @JsonSerialize(contentConverter = UUIDToAnyConverter::class) - @JsonDeserialize(contentConverter = AnyToUUIDConverter::class) - public val customList: List -) -``` - - - - -Following serializer will be generated under `com.example.generated.scalars` package. - -```kotlin -@Generated -public object UUIDSerializer : KSerializer { - private val converter: UUIDScalarConverter = UUIDScalarConverter() - - public override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", STRING) - - public override fun serialize(encoder: Encoder, `value`: UUID): Unit { - val encoded = converter.toJson(value) - encoder.encodeString(encoded.toString()) - } - - public override fun deserialize(decoder: Decoder): UUID { - val jsonDecoder = decoder as JsonDecoder - val element = jsonDecoder.decodeJsonElement() - val rawContent = element.jsonPrimitive.content - return converter.toScalar(rawContent) - } -} -``` - -Custom scalars fields will then be annotated with `@Serializable` annotation referencing the above serializer. - -```kotlin -@Generated -@Serializable -public data class Result( - @Serializable(with = UUIDSerializer::class) - public val custom: UUID, - public val customList: List<@Serializable(with = UUIDSerializer::class) UUID> -) -``` - - - - -See [Gradle](../plugins/gradle-plugin-tasks.mdx) and [Maven](../plugins/maven-plugin-goals.md) plugin documentation for additional details. diff --git a/website/docs/client/client-features.mdx b/website/docs/client/client-features.mdx index e805577091..4bfd3b8e42 100644 --- a/website/docs/client/client-features.mdx +++ b/website/docs/client/client-features.mdx @@ -173,6 +173,228 @@ data class DefaultBasicInterfaceImplementation( +## Custom Scalar Support + +By default, custom GraphQL scalars are serialized and [type-aliased](https://kotlinlang.org/docs/reference/type-aliases.html) +to a String. GraphQL Kotlin plugins also support custom serialization based on provided configuration. + +In order to automatically convert between custom GraphQL `UUID` scalar type and `java.util.UUID`, we first need to create +our custom `ScalarConverter`. + +```kotlin +package com.example.client + +import com.expediagroup.graphql.client.converter.ScalarConverter +import java.util.UUID + +class UUIDScalarConverter : ScalarConverter { + override fun toScalar(rawValue: Any): UUID = UUID.fromString(rawValue.toString()) + override fun toJson(value: UUID): Any = value.toString() +} +``` + +And then configure build plugin by specifying + +- Custom GraphQL scalar name +- Target JVM class name +- Converter that provides logic to map between GraphQL and Kotlin type + +```kotlin +graphql { + packageName = "com.example.generated" + endpoint = "http://localhost:8080/graphql" + customScalars = listOf(GraphQLScalar("UUID", "java.util.UUID", "com.example.UUIDScalarConverter")) +} +``` + +Custom scalar fields will then be automatically converted to a `java.util.UUID` type using appropriate converter/serializer. + + + + + +Following converters will be generated under `com.example.generated.scalars` package. + +```kotlin +@Generated +public class AnyToUUIDConverter : StdConverter() { + private val converter: UUIDScalarConverter = UUIDScalarConverter() + + public override fun convert(`value`: Any): UUID = converter.toScalar(value) +} + +@Generated +public class UUIDToAnyConverter : StdConverter() { + private val converter: UUIDScalarConverter = UUIDScalarConverter() + + public override fun convert(`value`: UUID): Any = converter.toJson(value) +} +``` + +Custom scalars fields will then be annotated with Jackson annotations referencing the above converters. + +```kotlin +@Generated +public data class Result( + @JsonSerialize(converter = UUIDToAnyConverter::class) + @JsonDeserialize(converter = AnyToUUIDConverter::class) + public val custom: UUID, + @JsonSerialize(contentConverter = UUIDToAnyConverter::class) + @JsonDeserialize(contentConverter = AnyToUUIDConverter::class) + public val customList: List +) +``` + + + + +Following serializer will be generated under `com.example.generated.scalars` package. + +```kotlin +@Generated +public object UUIDSerializer : KSerializer { + private val converter: UUIDScalarConverter = UUIDScalarConverter() + + public override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", STRING) + + public override fun serialize(encoder: Encoder, `value`: UUID): Unit { + val encoded = converter.toJson(value) + encoder.encodeString(encoded.toString()) + } + + public override fun deserialize(decoder: Decoder): UUID { + val jsonDecoder = decoder as JsonDecoder + val element = jsonDecoder.decodeJsonElement() + val rawContent = element.jsonPrimitive.content + return converter.toScalar(rawContent) + } +} +``` + +Custom scalars fields will then be annotated with `@Serializable` annotation referencing the above serializer. + +```kotlin +@Generated +@Serializable +public data class Result( + @Serializable(with = UUIDSerializer::class) + public val custom: UUID, + public val customList: List<@Serializable(with = UUIDSerializer::class) UUID> +) +``` + + + + +See [Gradle](../plugins/gradle-plugin-tasks.mdx) and [Maven](../plugins/maven-plugin-goals.md) plugin documentation for additional details. + +:::info +While custom scalars are most commonly represented using some primitive values (e.g. serializing UUID as String), it is +possible to use arbitrary objects representation as custom scalar. For example Apollo Federation relies on `_Any` scalar +to accept federated entity representations which is a JSON map containing `__typename` information and a number of additional +fields used to uniquely identify the target object. + + + + + +Jackson uses reflection to automatically serialize the objects. In order to rely on this behavior for custom scalars, +we simply need to implement a pass-through converter. + +```kotlin +class AnyScalarConverter : ScalarConverter { + override fun toScalar(rawValue: Any): Any = rawValue + override fun toJson(value: Any): Any = value +} +``` + +This will allow us to pass arbitrary objects as custom scalar inputs. Given following Federation type and `_entities` query + +``` +type Product @key(fields : "id") { + id: String! + name: String! +} + +query EntitiesQuery($representations: [_Any!]!) { + _entities(representations: $representations) { + __typename + ...on Product { name } + } + } +} +``` + +We can create corresponding `ProductEntityRepresentation` data class and use it in our generated query. + +```kotlin +data class ProductEntityRepresentation(val id: String) { + val __typename: String = "Product" +} + +val entityData = client.execute(EntitiesQuery(variables = EntitiesQuery.Variables(representations = listOf(ProductEntityRepresentation(id = "apollo-federation"))))) +``` + + + + +Using kotlinx-serialization we can use `JsonObject` to represent arbitrary objects as custom scalars. + +```kotlin +class AnyScalarConverter : ScalarConverter { + override fun toScalar(rawValue: Any): JsonObject = Json.parseToJsonElement(rawValue.toString()).jsonObject + override fun toJson(value: JsonObject): Any = value +} +``` + +This will allow us to pass arbitrary objects as custom scalar inputs. Given following Federation type and `_entities` query + +``` +type Product @key(fields : "id") { + id: String! + name: String! +} + +query EntitiesQuery($representations: [_Any!]!) { + _entities(representations: $representations) { + __typename + ...on Product { name } + } + } +} +``` + +We can then represent product entity representation as `JsonObject` and use it in our generated query. + +```kotlin +val entity = Json.decodeFromString( + """ + |{ + | "__typename": "Product", + | "id": "apollo-federation" + |} + """.trimMargin() +) + +val entityData = client.execute(EntitiesQuery(variables = EntitiesQuery.Variables(representations = listOf(entity)))) +``` + + + +::: + ## Default Enum Values Enums represent predefined set of values. Adding additional enum values could be a potentially breaking change as your