Skip to content

Commit 28e2288

Browse files
committed
[client] support non JSON primitive scalars
1 parent 9ac14e0 commit 28e2288

File tree

20 files changed

+546
-201
lines changed

20 files changed

+546
-201
lines changed

clients/graphql-kotlin-client-serialization/src/test/kotlin/com/expediagroup/graphql/client/serialization/data/scalars/UUIDSerializer.kt

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,47 @@ package com.expediagroup.graphql.client.serialization.data.scalars
1818

1919
import com.expediagroup.graphql.client.Generated
2020
import com.expediagroup.graphql.client.converter.ScalarConverter
21+
import kotlinx.serialization.ExperimentalSerializationApi
2122
import java.util.UUID
2223
import kotlinx.serialization.KSerializer
23-
import kotlinx.serialization.descriptors.PrimitiveKind.STRING
24-
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
2524
import kotlinx.serialization.descriptors.SerialDescriptor
25+
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
2626
import kotlinx.serialization.encoding.Decoder
2727
import kotlinx.serialization.encoding.Encoder
2828
import kotlinx.serialization.json.JsonDecoder
29+
import kotlinx.serialization.json.JsonPrimitive
2930
import kotlinx.serialization.json.jsonPrimitive
31+
import kotlinx.serialization.serializerOrNull
3032

3133
@Generated
3234
object UUIDSerializer : KSerializer<UUID> {
3335
private val converter: UUIDScalarConverter = UUIDScalarConverter()
3436

35-
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", STRING)
37+
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UUID")
3638

39+
@ExperimentalSerializationApi
3740
override fun serialize(encoder: Encoder, `value`: UUID) {
3841
val encoded = converter.toJson(value)
39-
encoder.encodeString(encoded.toString())
42+
val serializer = serializerOrNull(encoded::class.java)
43+
if (serializer != null) {
44+
encoder.encodeSerializableValue(serializer, encoded)
45+
} else {
46+
encoder.encodeString(encoded.toString())
47+
}
4048
}
4149

4250
override fun deserialize(decoder: Decoder): UUID {
4351
val jsonDecoder = decoder as JsonDecoder
44-
val element = jsonDecoder.decodeJsonElement()
45-
val rawContent = element.jsonPrimitive.content
52+
val rawContent: Any = when (val element = jsonDecoder.decodeJsonElement()) {
53+
is JsonPrimitive -> element.jsonPrimitive.content
54+
else -> element
55+
}
4656
return converter.toScalar(rawContent)
4757
}
4858
}
4959

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

examples/client/gradle-client/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ gradle clean build
1616

1717
## Running locally
1818

19+
* Start server in `server-client-example`, see project README for details
1920
* **[only works after project is build]** Run `Application.kt` directly from your IDE
2021
* Alternatively you can also use the Gradle application plugin by running `gradle run` from the command line
2122

examples/client/gradle-client/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ graphql {
3232
allowDeprecatedFields = true
3333
headers = mapOf("X-Custom-Header" to "My-Custom-Header")
3434
customScalars = listOf(
35-
GraphQLScalar("UUID", "java.util.UUID", "com.expediagroup.graphql.examples.client.gradle.UUIDScalarConverter"),
35+
GraphQLScalar("_Any", "kotlinx.serialization.json.JsonObject", "com.expediagroup.graphql.examples.client.gradle._AnyScalarConverter"),
3636
GraphQLScalar("Locale", "com.ibm.icu.util.ULocale", "com.expediagroup.graphql.examples.client.gradle.ULocaleScalarConverter"),
37+
GraphQLScalar("UUID", "java.util.UUID", "com.expediagroup.graphql.examples.client.gradle.UUIDScalarConverter"),
3738
)
3839
serializer = GraphQLSerializer.KOTLINX
3940
}
Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ package com.expediagroup.graphql.examples.client.gradle
1919
import com.expediagroup.graphql.client.ktor.GraphQLKtorClient
2020
import com.expediagroup.graphql.client.types.GraphQLClientResponse
2121
import com.expediagroup.graphql.generated.AddObjectMutation
22+
import com.expediagroup.graphql.generated.EntitiesQuery
2223
import com.expediagroup.graphql.generated.ExampleQuery
2324
import com.expediagroup.graphql.generated.HelloWorldQuery
2425
import com.expediagroup.graphql.generated.RetrieveObjectQuery
2526
import com.expediagroup.graphql.generated.UpdateObjectMutation
27+
import com.expediagroup.graphql.generated.entitiesquery.Product
2628
import com.expediagroup.graphql.generated.inputs.BasicObjectInput
2729
import com.expediagroup.graphql.generated.inputs.SimpleArgumentInput
2830
import io.ktor.client.HttpClient
@@ -32,10 +34,19 @@ import io.ktor.client.plugins.logging.LogLevel
3234
import io.ktor.client.plugins.logging.Logger
3335
import io.ktor.client.plugins.logging.Logging
3436
import kotlinx.coroutines.runBlocking
37+
import kotlinx.serialization.decodeFromString
38+
import kotlinx.serialization.json.Json
39+
import kotlinx.serialization.json.JsonObject
3540
import java.net.URL
3641
import java.util.concurrent.TimeUnit
3742

3843
fun main() {
44+
/*
45+
* ************************************************************
46+
* Make sure to start example server before running this code
47+
* https://github.com/dariuszkuc/graphql-kotlin/blob/client-custom-scalars/examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/Application.kt
48+
* ************************************************************
49+
*/
3950
val httpClient = HttpClient(engineFactory = OkHttp) {
4051
engine {
4152
config {
@@ -57,6 +68,8 @@ fun main() {
5768
runBlocking {
5869
val helloWorldQuery = HelloWorldQuery(variables = HelloWorldQuery.Variables())
5970
val helloWorldResult = client.execute(helloWorldQuery)
71+
println("\tquery without parameters result: ${helloWorldResult.data?.helloWorld}")
72+
6073
val helloWorldResultImplicit: GraphQLClientResponse<HelloWorldQuery.Result> = client.execute(helloWorldQuery)
6174

6275
val results = client.execute(
@@ -68,7 +81,7 @@ fun main() {
6881

6982
val resultsNoParam = results[0].data as? HelloWorldQuery.Result
7083
val resultsWithParam = results[1].data as? HelloWorldQuery.Result
71-
println("\tquery without parameters result: ${resultsNoParam?.helloWorld}")
84+
println("\tquery with null name result: ${resultsNoParam?.helloWorld}")
7285
println("\tquery with parameters result: ${resultsWithParam?.helloWorld}")
7386
}
7487

@@ -95,5 +108,23 @@ fun main() {
95108
println("\tretrieved example list: [${exampleData.data?.listQuery?.joinToString { it.name }}]")
96109
}
97110

111+
println("entities query")
112+
runBlocking {
113+
val entity = Json.decodeFromString<JsonObject>(
114+
"""
115+
|{
116+
| "__typename": "Product",
117+
| "id": "apollo-federation"
118+
|}
119+
""".trimMargin()
120+
)
121+
val entityData = client.execute(EntitiesQuery(variables = EntitiesQuery.Variables(representations = listOf(entity))))
122+
val product = entityData.data?._entities?.get(0) as? Product
123+
println("\tretrieved product SKU: ${product?.sku}")
124+
println("\tretrieved product package: ${product?.`package`}")
125+
println("\tretrieved product variation ID: ${product?.variation?.id}")
126+
println("\tretrieved product dimensions: size=${product?.dimensions?.size}, weight=${product?.dimensions?.weight}")
127+
}
128+
98129
client.close()
99130
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2022 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.examples.client.gradle
18+
19+
import com.expediagroup.graphql.client.converter.ScalarConverter
20+
import kotlinx.serialization.json.Json
21+
import kotlinx.serialization.json.JsonObject
22+
import kotlinx.serialization.json.jsonObject
23+
24+
class _AnyScalarConverter : ScalarConverter<JsonObject> {
25+
26+
override fun toScalar(rawValue: Any): JsonObject = Json.parseToJsonElement(rawValue.toString()).jsonObject
27+
28+
override fun toJson(value: JsonObject): Any = value
29+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
query EntitiesQuery($representations: [_Any!]!) {
2+
_entities(representations: $representations) {
3+
__typename
4+
...on Product {sku package variation { id } dimensions { size weight }
5+
}
6+
}
7+
}

examples/client/maven-client/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ you can build it as
2424

2525
## Running locally
2626

27+
* Start server in `server-client-example`, see project README for details
2728
* **[only works after project is build]** Run `Application.kt` directly from your IDE
2829
* Alternatively you can also use the Maven exec plugin by running `./mvnw exec:java` from the command line
2930

examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/Application.kt

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,11 @@
1616

1717
package com.expediagroup.graphql.examples.client.server
1818

19-
import com.expediagroup.graphql.examples.client.server.scalars.graphqlULocaleType
20-
import com.expediagroup.graphql.examples.client.server.scalars.graphqlUUIDType
21-
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
22-
import com.ibm.icu.util.ULocale
23-
import graphql.schema.GraphQLType
2419
import org.springframework.boot.autoconfigure.SpringBootApplication
2520
import org.springframework.boot.runApplication
26-
import org.springframework.context.annotation.Bean
27-
import java.util.UUID
28-
import kotlin.reflect.KType
2921

3022
@SpringBootApplication
31-
class Application {
32-
33-
@Bean
34-
fun customHooks(): SchemaGeneratorHooks = object : SchemaGeneratorHooks {
35-
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier) {
36-
UUID::class -> graphqlUUIDType
37-
ULocale::class -> graphqlULocaleType
38-
else -> null
39-
}
40-
}
41-
}
23+
class Application
4224

4325
fun main(args: Array<String>) {
4426
runApplication<Application>(*args)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.expediagroup.graphql.examples.client.server
2+
3+
import com.expediagroup.graphql.examples.client.server.scalars.graphqlULocaleType
4+
import com.expediagroup.graphql.examples.client.server.scalars.graphqlUUIDType
5+
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorHooks
6+
import com.expediagroup.graphql.generator.federation.execution.FederatedTypeResolver
7+
import com.ibm.icu.util.ULocale
8+
import graphql.schema.GraphQLType
9+
import org.springframework.stereotype.Component
10+
import java.util.UUID
11+
import kotlin.reflect.KType
12+
13+
@Component
14+
class CustomFederatedHooks(resolvers: List<FederatedTypeResolver<*>>) : FederatedSchemaGeneratorHooks(resolvers, true) {
15+
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier) {
16+
UUID::class -> graphqlUUIDType
17+
ULocale::class -> graphqlULocaleType
18+
else -> super.willGenerateGraphQLType(type)
19+
}
20+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.expediagroup.graphql.examples.client.server
2+
3+
import com.expediagroup.graphql.generator.annotations.GraphQLName
4+
import com.expediagroup.graphql.generator.federation.directives.ExtendsDirective
5+
import com.expediagroup.graphql.generator.federation.directives.ExternalDirective
6+
import com.expediagroup.graphql.generator.federation.directives.FieldSet
7+
import com.expediagroup.graphql.generator.federation.directives.InaccessibleDirective
8+
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
9+
import com.expediagroup.graphql.generator.federation.directives.OverrideDirective
10+
import com.expediagroup.graphql.generator.federation.directives.ProvidesDirective
11+
import com.expediagroup.graphql.generator.federation.directives.ShareableDirective
12+
import com.expediagroup.graphql.generator.federation.directives.TagDirective
13+
import com.expediagroup.graphql.generator.federation.execution.FederatedTypeResolver
14+
import com.expediagroup.graphql.generator.scalars.ID
15+
import com.expediagroup.graphql.server.operations.Query
16+
import graphql.schema.DataFetchingEnvironment
17+
import org.springframework.stereotype.Component
18+
19+
@Component
20+
class FederatedQuery : Query {
21+
22+
fun product(id: ID) = Product.byID(id)
23+
}
24+
25+
@KeyDirective(fields = FieldSet("id"))
26+
// @KeyDirective(fields = FieldSet("sku package"))
27+
// @KeyDirective(fields = FieldSet("sku variation { id }"))
28+
data class Product(
29+
val id: ID,
30+
val sku: String? = null,
31+
@GraphQLName("package")
32+
val pkg: String? = null,
33+
val variation: ProductVariation? = null,
34+
val dimensions: ProductDimension? = null,
35+
@ProvidesDirective(FieldSet("totalProductsCreated"))
36+
val createdBy: User? = null,
37+
@TagDirective("internal")
38+
val notes: String? = null
39+
) {
40+
companion object {
41+
fun byID(id: ID) = PRODUCTS.find { it.id.value == id.value }
42+
private fun bySkuAndPackage(sku: String, pkg: String) = PRODUCTS.find { it.sku == sku && it.pkg == pkg }
43+
private fun bySkuAndVariation(sku: String, variationId: String) =
44+
PRODUCTS.find { it.sku == sku && it.variation?.id?.value == variationId }
45+
46+
fun byReference(ref: Map<String, Any>): Product? {
47+
val id = ref["id"]?.toString()
48+
val sku = ref["sku"]?.toString()
49+
val pkg = ref["package"]?.toString()
50+
val variation = ref["variation"]
51+
val variationId = if (variation is Map<*, *>) {
52+
variation["id"].toString()
53+
} else null
54+
55+
return when {
56+
id != null -> byID(ID(id))
57+
sku != null && pkg != null -> bySkuAndPackage(sku, pkg)
58+
sku != null && variationId != null -> bySkuAndVariation(sku, variationId)
59+
else -> throw RuntimeException("invalid entity reference")
60+
}
61+
}
62+
}
63+
}
64+
65+
val PRODUCTS = listOf(
66+
Product(
67+
ID("apollo-federation"),
68+
"federation",
69+
"@apollo/federation",
70+
ProductVariation(ID("OSS")),
71+
ProductDimension("small", 1.0f),
72+
User(email = "[email protected]", name = "support", totalProductsCreated = 1337)
73+
),
74+
Product(
75+
ID("apollo-studio"),
76+
"studio",
77+
"",
78+
ProductVariation(ID("platform")),
79+
ProductDimension("small", 1.0f),
80+
User(email = "[email protected]", name = "support", totalProductsCreated = 1337)
81+
)
82+
)
83+
84+
@ShareableDirective
85+
data class ProductDimension(
86+
val size: String? = null,
87+
val weight: Float? = null,
88+
@InaccessibleDirective
89+
val unit: String? = null
90+
)
91+
92+
data class ProductVariation(
93+
val id: ID
94+
)
95+
96+
@KeyDirective(fields = FieldSet("email"))
97+
@ExtendsDirective
98+
data class User(
99+
@ExternalDirective
100+
val email: String,
101+
@OverrideDirective(from = "users")
102+
val name: String,
103+
@ExternalDirective
104+
val totalProductsCreated: Int? = null
105+
)
106+
107+
@Component
108+
class ProductsResolver : FederatedTypeResolver<Product> {
109+
override val typeName: String = "Product"
110+
111+
override suspend fun resolve(
112+
environment: DataFetchingEnvironment,
113+
representations: List<Map<String, Any>>
114+
): List<Product?> = representations.map {
115+
Product.byReference(it)
116+
}
117+
}
118+
119+
@Component
120+
class UserResolver : FederatedTypeResolver<User> {
121+
override val typeName: String = "User"
122+
123+
override suspend fun resolve(
124+
environment: DataFetchingEnvironment,
125+
representations: List<Map<String, Any>>
126+
): List<User?> {
127+
return representations.map {
128+
val email = it["email"]?.toString() ?: throw RuntimeException("invalid entity reference")
129+
User(email = email, name = "default", totalProductsCreated = 1337)
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)