Skip to content

Commit bfac731

Browse files
authored
[client] fix handling of custom scalar inputs (#1266)
Update client generation logic to correctly annotate custom scalars when they are used as input fields. Due to the extra wrapper layer, optional input functionality is not supported with custom scalars at this point. This PR also fixes client generation logic to map GraphQL Floats to Kotlin Double (instead of a Float). Related: * partially resolves #1263
1 parent d7ebb8c commit bfac731

File tree

73 files changed

+1059
-137
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+1059
-137
lines changed

examples/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ subprojects {
3636
this.ext[key.toString()] = value
3737
}
3838

39-
val kotlinVersion: String by project
39+
val icuVersion: String by project
4040
val junitVersion: String by project
41+
val kotlinVersion: String by project
4142
val kotlinCoroutinesVersion: String by project
4243

4344
val detektVersion: String by project
@@ -51,7 +52,7 @@ subprojects {
5152
implementation(kotlin("stdlib", kotlinVersion))
5253
implementation(kotlin("reflect", kotlinVersion))
5354
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinCoroutinesVersion")
54-
implementation("com.ibm.icu:icu4j:69.1")
55+
implementation("com.ibm.icu:icu4j:$icuVersion")
5556
testImplementation(kotlin("test-junit5", kotlinVersion))
5657
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion")
5758
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ fun main() {
8787

8888
println("additional examples")
8989
runBlocking {
90-
val exampleData = client.execute(ExampleQuery(variables = ExampleQuery.Variables(simpleCriteria = SimpleArgumentInput(max = 1.0f))))
90+
val exampleData = client.execute(ExampleQuery(variables = ExampleQuery.Variables(simpleCriteria = SimpleArgumentInput(max = 1.0))))
9191
println("\tretrieved interface: ${exampleData.data?.interfaceQuery} ")
9292
println("\tretrieved union: ${exampleData.data?.unionQuery} ")
9393
println("\tretrieved enum: ${exampleData.data?.enumQuery} ")

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ fun main() {
7676

7777
println("additional examples")
7878
runBlocking {
79-
val exampleData = client.execute(ExampleQuery(variables = ExampleQuery.Variables(simpleCriteria = OptionalInput.Defined(SimpleArgumentInput(max = OptionalInput.Defined(1.0f))))))
79+
val exampleData = client.execute(ExampleQuery(variables = ExampleQuery.Variables(simpleCriteria = OptionalInput.Defined(SimpleArgumentInput(max = OptionalInput.Defined(1.0))))))
8080
println("\tretrieved interface: ${exampleData.data?.interfaceQuery} ")
8181
println("\tretrieved union: ${exampleData.data?.unionQuery} ")
8282
println("\tretrieved enum: ${exampleData.data?.enumQuery} ")

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ springVersion = 5.3.9
3434

3535
# test dependency versions
3636
compileTestingVersion = 1.4.3
37+
icuVersion=69.1
3738
junitVersion = 5.7.2
3839
mockkVersion = 1.12.0
3940
mustacheVersion = 0.9.10

plugins/client/graphql-kotlin-client-generator/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ description = "GraphQL Kotlin common utilities to generate a client."
22

33
val compileTestingVersion: String by project
44
val graphQLJavaVersion: String by project
5+
val icuVersion: String by project
56
val jacksonVersion: String by project
67
val junitVersion: String by project
78
val kotlinPoetVersion: String by project
@@ -26,6 +27,7 @@ dependencies {
2627
testImplementation("com.github.tomakehurst:wiremock-jre8:$wireMockVersion")
2728
testImplementation("com.github.tschuchortdev:kotlin-compile-testing:$compileTestingVersion")
2829
testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion")
30+
testImplementation("com.ibm.icu:icu4j:$icuVersion")
2931
}
3032

3133
tasks {

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGenerator.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ import graphql.schema.idl.SchemaParser
3737
import graphql.schema.idl.TypeDefinitionRegistry
3838
import kotlinx.serialization.Required
3939
import kotlinx.serialization.Serializable
40+
import org.slf4j.LoggerFactory
4041
import java.io.File
4142

4243
private const val CORE_TYPES_PACKAGE = "com.expediagroup.graphql.client.types"
44+
internal val LOGGER = LoggerFactory.getLogger(GraphQLClientGenerator::class.java)
4345

4446
/**
4547
* GraphQL client code generator that uses [KotlinPoet](https://github.com/square/kotlinpoet) to generate Kotlin classes based on the specified GraphQL queries.

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateGraphQLInputObjectTypeSpec.kt

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.expediagroup.graphql.plugin.client.generator.types
1919
import com.expediagroup.graphql.client.Generated
2020
import com.expediagroup.graphql.plugin.client.generator.GraphQLClientGeneratorContext
2121
import com.expediagroup.graphql.plugin.client.generator.GraphQLSerializer
22+
import com.expediagroup.graphql.plugin.client.generator.LOGGER
2223
import com.squareup.kotlinpoet.ClassName
2324
import com.squareup.kotlinpoet.CodeBlock
2425
import com.squareup.kotlinpoet.FunSpec
@@ -36,7 +37,8 @@ import kotlinx.serialization.Serializable
3637
* Generate [TypeSpec] data class from the specified input object definition where are fields are mapped to corresponding Kotlin property.
3738
*/
3839
internal fun generateGraphQLInputObjectTypeSpec(context: GraphQLClientGeneratorContext, inputObjectDefinition: InputObjectTypeDefinition): TypeSpec {
39-
val inputObjectTypeSpecBuilder = TypeSpec.classBuilder(inputObjectDefinition.name)
40+
val inputTypeName = inputObjectDefinition.name
41+
val inputObjectTypeSpecBuilder = TypeSpec.classBuilder(inputTypeName)
4042
.addModifiers(KModifier.DATA)
4143
.addAnnotation(Generated::class)
4244
inputObjectDefinition.description?.content?.let { kdoc ->
@@ -49,43 +51,67 @@ internal fun generateGraphQLInputObjectTypeSpec(context: GraphQLClientGeneratorC
4951

5052
val constructorBuilder = FunSpec.constructorBuilder()
5153
inputObjectDefinition.inputValueDefinitions.forEach { fieldDefinition ->
52-
val kotlinFieldType = generateTypeName(context, fieldDefinition.type)
53-
val fieldName = fieldDefinition.name
54+
val kotlinFieldTypeName = generateTypeName(context, fieldDefinition.type)
5455

55-
val inputFieldType = kotlinFieldType.wrapOptionalInputType(context)
56-
val inputPropertySpecBuilder = PropertySpec.builder(fieldName, inputFieldType)
57-
.initializer(fieldName)
58-
fieldDefinition.description?.content?.let { kdoc ->
59-
inputPropertySpecBuilder.addKdoc("%L", kdoc)
56+
val (rawType, isList) = unwrapRawType(kotlinFieldTypeName)
57+
val isCustomScalar = context.isCustomScalar(rawType)
58+
val shouldWrapInOptional = shouldWrapInOptional(kotlinFieldTypeName, context)
59+
60+
val inputFieldType = if (!isCustomScalar && shouldWrapInOptional) {
61+
kotlinFieldTypeName.wrapOptionalInputType(context)
62+
} else {
63+
kotlinFieldTypeName
6064
}
6165

62-
val inputPropertySpec = inputPropertySpecBuilder.build()
66+
val inputPropertySpec = PropertySpec.builder(fieldDefinition.name, inputFieldType)
67+
.initializer(fieldDefinition.name)
68+
.also { builder ->
69+
fieldDefinition.description?.content?.let { kdoc ->
70+
builder.addKdoc("%L", kdoc)
71+
}
72+
73+
if (isCustomScalar) {
74+
builder.addAnnotations(generateCustomScalarPropertyAnnotations(context, rawType, isList))
75+
76+
if (shouldWrapInOptional) {
77+
LOGGER.warn(
78+
"Operation ${context.operationName} specifies optional custom scalar as input - ${fieldDefinition.name} in Variables. " +
79+
"Currently custom scalars do not work with optional wrappers."
80+
)
81+
builder.addKdoc("\nNOTE: This field was not wrapped in optional as currently custom scalars do not work with optional wrappers.")
82+
}
83+
}
84+
}
85+
.build()
6386
inputObjectTypeSpecBuilder.addProperty(inputPropertySpec)
6487

65-
val inputParameterSpec = ParameterSpec.builder(inputPropertySpec.name, inputPropertySpec.type)
66-
if (kotlinFieldType.isNullable) {
67-
inputParameterSpec.defaultValue(nullableDefaultValueCodeBlock(context))
68-
}
69-
constructorBuilder.addParameter(inputParameterSpec.build())
88+
constructorBuilder.addParameter(
89+
ParameterSpec.builder(inputPropertySpec.name, inputPropertySpec.type)
90+
.also { builder ->
91+
if (kotlinFieldTypeName.isNullable) {
92+
builder.defaultValue(nullableDefaultValueCodeBlock(context, isCustomScalar))
93+
}
94+
}
95+
.build()
96+
)
7097
}
7198
inputObjectTypeSpecBuilder.primaryConstructor(constructorBuilder.build())
7299

73100
return inputObjectTypeSpecBuilder.build()
74101
}
75102

76-
internal fun TypeName.wrapOptionalInputType(context: GraphQLClientGeneratorContext): TypeName = if (this.isNullable && context.useOptionalInputWrapper) {
103+
internal fun shouldWrapInOptional(type: TypeName, context: GraphQLClientGeneratorContext) = type.isNullable && context.useOptionalInputWrapper
104+
105+
internal fun TypeName.wrapOptionalInputType(context: GraphQLClientGeneratorContext): TypeName =
77106
if (context.serializer == GraphQLSerializer.JACKSON) {
78107
ClassName("com.expediagroup.graphql.client.jackson.types", "OptionalInput")
79108
.parameterizedBy(this.copy(nullable = false))
80109
} else {
81110
ClassName("com.expediagroup.graphql.client.serialization.types", "OptionalInput")
82111
.parameterizedBy(this.copy(nullable = false))
83112
}
84-
} else {
85-
this
86-
}
87113

88-
internal fun nullableDefaultValueCodeBlock(context: GraphQLClientGeneratorContext): CodeBlock = if (context.useOptionalInputWrapper) {
114+
internal fun nullableDefaultValueCodeBlock(context: GraphQLClientGeneratorContext, isCustomScalar: Boolean): CodeBlock = if (context.useOptionalInputWrapper && !isCustomScalar) {
89115
if (context.serializer == GraphQLSerializer.JACKSON) {
90116
CodeBlock.of("%M", MemberName("com.expediagroup.graphql.client.jackson.types", "OptionalInput.Undefined"))
91117
} else {

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generatePropertySpecs.kt

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.squareup.kotlinpoet.AnnotationSpec
2727
import com.squareup.kotlinpoet.KModifier
2828
import com.squareup.kotlinpoet.ParameterizedTypeName
2929
import com.squareup.kotlinpoet.PropertySpec
30+
import com.squareup.kotlinpoet.TypeName
3031
import graphql.Directives.DeprecatedDirective
3132
import graphql.language.Field
3233
import graphql.language.FieldDefinition
@@ -57,47 +58,16 @@ internal fun generatePropertySpecs(
5758
throw MissingArgumentException(context.operationName, objectName, selectedField.name, missingRequiredArguments)
5859
}
5960

60-
val nullable = fieldDefinition.type !is NonNullType
6161
val kotlinFieldType = generateTypeName(context, fieldDefinition.type, selectedField.selectionSet)
6262
val fieldName = selectedField.alias ?: fieldDefinition.name
6363

64-
val propertySpecBuilder = PropertySpec.builder(fieldName, kotlinFieldType.copy(nullable = nullable))
64+
val propertySpecBuilder = PropertySpec.builder(fieldName, kotlinFieldType)
6565
if (!abstract) {
6666
propertySpecBuilder.initializer(fieldName)
67-
68-
val (unwrappedFieldType, isList) = if (kotlinFieldType is ParameterizedTypeName) {
69-
kotlinFieldType.typeArguments.first() to true
70-
} else {
71-
kotlinFieldType to false
72-
}
73-
74-
if (context.isCustomScalar(unwrappedFieldType)) {
75-
val converterInfo = context.scalarClassToConverterTypeSpecs[unwrappedFieldType]
76-
when {
77-
converterInfo is ScalarConverterInfo.JacksonConvertersInfo -> {
78-
val annotationMember = if (isList) {
79-
"contentConverter"
80-
} else {
81-
"converter"
82-
}
83-
propertySpecBuilder.addAnnotation(
84-
AnnotationSpec.builder(JsonSerialize::class)
85-
.addMember("$annotationMember = %T::class", converterInfo.serializerClassName)
86-
.build()
87-
)
88-
propertySpecBuilder.addAnnotation(
89-
AnnotationSpec.builder(JsonDeserialize::class)
90-
.addMember("$annotationMember = %T::class", converterInfo.deserializerClassName)
91-
.build()
92-
)
93-
}
94-
converterInfo is ScalarConverterInfo.KotlinxSerializerInfo && !isList -> {
95-
propertySpecBuilder.addAnnotation(
96-
AnnotationSpec.builder(Serializable::class)
97-
.addMember("with = %T::class", converterInfo.serializerClassName)
98-
.build()
99-
)
100-
}
67+
val (rawType, isList) = unwrapRawType(kotlinFieldType)
68+
if (context.isCustomScalar(rawType)) {
69+
generateCustomScalarPropertyAnnotations(context, rawType, isList).forEach { scalarAnnotation ->
70+
propertySpecBuilder.addAnnotation(scalarAnnotation)
10171
}
10272
}
10373
} else {
@@ -122,3 +92,51 @@ internal fun generatePropertySpecs(
12292
}
12393
propertySpecBuilder.build()
12494
}
95+
96+
internal fun unwrapRawType(type: TypeName): Pair<TypeName, Boolean> {
97+
val rawType = type.unwrapNullableType()
98+
return if (rawType is ParameterizedTypeName) {
99+
rawType.typeArguments.first() to true
100+
} else {
101+
rawType to false
102+
}
103+
}
104+
105+
private fun TypeName.unwrapNullableType(): TypeName = if (this.isNullable) {
106+
this.copy(nullable = false)
107+
} else {
108+
this
109+
}
110+
111+
internal fun generateCustomScalarPropertyAnnotations(context: GraphQLClientGeneratorContext, rawType: TypeName, isList: Boolean): List<AnnotationSpec> {
112+
val result = mutableListOf<AnnotationSpec>()
113+
val converterInfo = context.scalarClassToConverterTypeSpecs[rawType]
114+
when {
115+
converterInfo is ScalarConverterInfo.JacksonConvertersInfo -> {
116+
val annotationMember = if (isList) {
117+
"contentConverter"
118+
} else {
119+
"converter"
120+
}
121+
result.add(
122+
AnnotationSpec.builder(JsonSerialize::class)
123+
.addMember("$annotationMember = %T::class", converterInfo.serializerClassName)
124+
.build()
125+
)
126+
result.add(
127+
AnnotationSpec.builder(JsonDeserialize::class)
128+
.addMember("$annotationMember = %T::class", converterInfo.deserializerClassName)
129+
.build()
130+
)
131+
}
132+
converterInfo is ScalarConverterInfo.KotlinxSerializerInfo && !isList -> {
133+
result.add(
134+
AnnotationSpec.builder(Serializable::class)
135+
.addMember("with = %T::class", converterInfo.serializerClassName)
136+
.build()
137+
)
138+
}
139+
}
140+
141+
return result
142+
}

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateTypeName.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import com.expediagroup.graphql.plugin.client.generator.extensions.findFragmentD
2424
import com.squareup.kotlinpoet.AnnotationSpec
2525
import com.squareup.kotlinpoet.BOOLEAN
2626
import com.squareup.kotlinpoet.ClassName
27-
import com.squareup.kotlinpoet.FLOAT
27+
import com.squareup.kotlinpoet.DOUBLE
2828
import com.squareup.kotlinpoet.INT
2929
import com.squareup.kotlinpoet.LIST
3030
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
@@ -59,7 +59,7 @@ internal fun generateTypeName(context: GraphQLClientGeneratorContext, graphQLTyp
5959
is NamedNode<*> -> when (graphQLType.name) {
6060
Scalars.GraphQLString.name -> STRING
6161
Scalars.GraphQLInt.name -> INT
62-
Scalars.GraphQLFloat.name -> FLOAT
62+
Scalars.GraphQLFloat.name -> DOUBLE
6363
Scalars.GraphQLBoolean.name -> BOOLEAN
6464
else -> generateCustomClassName(context, graphQLType, selectionSet)
6565
}

plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/types/generateVariableTypeSpec.kt

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.expediagroup.graphql.plugin.client.generator.types
1919
import com.expediagroup.graphql.client.Generated
2020
import com.expediagroup.graphql.plugin.client.generator.GraphQLClientGeneratorContext
2121
import com.expediagroup.graphql.plugin.client.generator.GraphQLSerializer
22+
import com.expediagroup.graphql.plugin.client.generator.LOGGER
2223
import com.squareup.kotlinpoet.FunSpec
2324
import com.squareup.kotlinpoet.KModifier
2425
import com.squareup.kotlinpoet.ParameterSpec
@@ -31,7 +32,8 @@ import kotlinx.serialization.Serializable
3132
* Generate [TypeSpec] data class wrapper for variables used within the target query.
3233
*/
3334
internal fun generateVariableTypeSpec(context: GraphQLClientGeneratorContext, variableDefinitions: List<VariableDefinition>): TypeSpec? {
34-
val variableTypeSpec = TypeSpec.classBuilder("Variables")
35+
val variablesTypeName = "Variables"
36+
val variableTypeSpec = TypeSpec.classBuilder(variablesTypeName)
3537
.addModifiers(KModifier.DATA)
3638
.addAnnotation(Generated::class)
3739
if (context.serializer == GraphQLSerializer.KOTLINX) {
@@ -41,16 +43,42 @@ internal fun generateVariableTypeSpec(context: GraphQLClientGeneratorContext, va
4143
val constructorSpec = FunSpec.constructorBuilder()
4244
variableDefinitions.forEach { variableDef ->
4345
val kotlinTypeName = generateTypeName(context, variableDef.type)
44-
val variableTypeName = kotlinTypeName.wrapOptionalInputType(context)
4546

46-
val parameterBuilder = ParameterSpec.builder(variableDef.name, variableTypeName)
47-
if (kotlinTypeName.isNullable) {
48-
parameterBuilder.defaultValue(nullableDefaultValueCodeBlock(context))
47+
val (rawType, isList) = unwrapRawType(kotlinTypeName)
48+
val isCustomScalar = context.isCustomScalar(rawType)
49+
val shouldWrapInOptional = shouldWrapInOptional(kotlinTypeName, context)
50+
51+
val variableTypeName = if (!isCustomScalar && shouldWrapInOptional) {
52+
kotlinTypeName.wrapOptionalInputType(context)
53+
} else {
54+
kotlinTypeName
4955
}
50-
constructorSpec.addParameter(parameterBuilder.build())
51-
variableTypeSpec.addProperty(
52-
PropertySpec.builder(variableDef.name, variableTypeName)
53-
.initializer(variableDef.name)
56+
57+
val variable = PropertySpec.builder(variableDef.name, variableTypeName)
58+
.initializer(variableDef.name)
59+
.also { builder ->
60+
if (isCustomScalar) {
61+
builder.addAnnotations(generateCustomScalarPropertyAnnotations(context, rawType, isList))
62+
63+
if (shouldWrapInOptional) {
64+
LOGGER.warn(
65+
"Operation ${context.operationName} specifies optional custom scalar as input - ${variableDef.name} in Variables. " +
66+
"Currently custom scalars do not work with optional wrappers."
67+
)
68+
builder.addKdoc("NOTE: This field was not wrapped in optional as currently custom scalars do not work with optional wrappers.")
69+
}
70+
}
71+
}
72+
.build()
73+
variableTypeSpec.addProperty(variable)
74+
75+
constructorSpec.addParameter(
76+
ParameterSpec.builder(variable.name, variable.type)
77+
.also { builder ->
78+
if (kotlinTypeName.isNullable) {
79+
builder.defaultValue(nullableDefaultValueCodeBlock(context, isCustomScalar))
80+
}
81+
}
5482
.build()
5583
)
5684
}

0 commit comments

Comments
 (0)