Skip to content

Commit d468c72

Browse files
author
Cole Turner
authored
feat: add NullableInputVariableSerializer (#557)
* feat: add NullableInputVariableSerializer * fix reference of interface for input value serializer * refactor: internalize serialize assignment * fix nullable params * remove unnecessary condition * fix constructors
1 parent d91aac3 commit d468c72

File tree

6 files changed

+182
-55
lines changed

6 files changed

+182
-55
lines changed

graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequest.kt

+15-4
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,27 @@ import graphql.schema.Coercing
2626
class GraphQLQueryRequest @JvmOverloads constructor(
2727
val query: GraphQLQuery,
2828
val projection: BaseProjectionNode? = null,
29-
scalars: Map<Class<*>, Coercing<*, *>>? = null
29+
options: GraphQLQueryRequestOptions? = null
3030
) {
3131

3232
private var selectionSet: SelectionSet? = null
33-
34-
@JvmOverloads constructor(query: GraphQLQuery, selectionSet: SelectionSet, scalars: Map<Class<*>, Coercing<*, *>>? = null) : this(query = query, scalars = scalars) {
33+
constructor(query: GraphQLQuery, projection: BaseProjectionNode, scalars: Map<Class<*>, Coercing<*, *>>) : this(query = query, projection = projection, options = GraphQLQueryRequestOptions(scalars = scalars))
34+
constructor(query: GraphQLQuery, selectionSet: SelectionSet, scalars: Map<Class<*>, Coercing<*, *>>? = null) : this(query = query, projection = null, options = GraphQLQueryRequestOptions(scalars = scalars ?: emptyMap())) {
3535
this.selectionSet = selectionSet
3636
}
37+
class GraphQLQueryRequestOptions(val scalars: Map<Class<*>, Coercing<*, *>> = emptyMap()) {
38+
// When enabled, input values that are derived from properties
39+
// whose values are null will be serialized in the query request
40+
val allowNullablePropertyInputValues = false
41+
}
42+
43+
val inputValueSerializer =
44+
if (options?.allowNullablePropertyInputValues == true) {
45+
NullableInputValueSerializer(options.scalars)
46+
} else {
47+
InputValueSerializer(options?.scalars ?: emptyMap())
48+
}
3749

38-
val inputValueSerializer = InputValueSerializer(scalars ?: emptyMap())
3950
val projectionSerializer = ProjectionSerializer(inputValueSerializer)
4051

4152
fun serialize(): String {

graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/InputValueSerializer.kt

+60-46
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,15 @@ import java.time.LocalDate
3535
import java.time.LocalDateTime
3636
import java.time.LocalTime
3737
import java.time.OffsetDateTime
38-
import java.util.Currency
39-
import java.util.Date
40-
import java.util.TimeZone
38+
import java.util.*
39+
import kotlin.reflect.KClass
4140
import kotlin.reflect.full.allSuperclasses
4241
import kotlin.reflect.full.hasAnnotation
4342
import kotlin.reflect.full.memberProperties
4443
import kotlin.reflect.jvm.isAccessible
4544

46-
/**
47-
* Marks this property invisible for input value serialization.
48-
*/
49-
@Target(AnnotationTarget.PROPERTY)
50-
internal annotation class Transient
51-
52-
interface InputValue {
53-
fun inputValues(): List<Pair<String, Any?>>
54-
}
55-
56-
class InputValueSerializer(private val scalars: Map<Class<*>, Coercing<*, *>> = emptyMap()) {
45+
open class InputValueSerializer(private val scalars: Map<Class<*>, Coercing<*, *>> = emptyMap()) :
46+
InputValueSerializerInterface {
5747
companion object {
5848
private val toStringClasses = setOf(
5949
String::class,
@@ -68,84 +58,115 @@ class InputValueSerializer(private val scalars: Map<Class<*>, Coercing<*, *>> =
6858
)
6959
}
7060

71-
fun serialize(input: Any?): String {
61+
override fun serialize(input: Any?): String {
7262
return AstPrinter.printAst(toValue(input))
7363
}
7464

75-
fun toValue(input: Any?): Value<*> {
65+
override fun toValue(input: Any?): Value<*> {
7666
if (input == null) {
7767
return NullValue.newNullValue().build()
7868
}
7969

70+
val optionalValue = getOptionalValue(input)
71+
72+
if (optionalValue.isPresent) {
73+
return optionalValue.get()
74+
}
75+
76+
val classes = (sequenceOf(input::class) + input::class.allSuperclasses.asSequence()) - Any::class
77+
val propertyValues = getPropertyValues(classes, input)
78+
79+
val objectFields = propertyValues.asSequence()
80+
.filter { (_, value) -> value != null }
81+
.map { (name, value) -> ObjectField(name, toValue(value)) }
82+
.toList()
83+
return ObjectValue.newObjectValue()
84+
.objectFields(objectFields)
85+
.build()
86+
}
87+
88+
protected fun getOptionalValue(input: Any): Optional<Value<*>> {
8089
if (input is Value<*>) {
81-
return input
90+
return Optional.of(input)
8291
}
8392

8493
for (scalar in scalars.keys) {
8594
if (input::class.java == scalar || scalar.isAssignableFrom(input::class.java)) {
86-
return scalars[scalar]!!.valueToLiteral(input)
95+
return Optional.of(scalars[scalar]!!.valueToLiteral(input))
8796
}
8897
}
8998

9099
if (input::class in toStringClasses) {
91-
return StringValue.of(input.toString())
100+
return Optional.of(StringValue.of(input.toString()))
92101
}
93102

94103
if (input is String) {
95-
return StringValue.of(input)
104+
return Optional.of(StringValue.of(input))
96105
}
97106

98107
if (input is Float) {
99-
return FloatValue.of(input.toDouble())
108+
return Optional.of(FloatValue.of(input.toDouble()))
100109
}
101110

102111
if (input is Double) {
103-
return FloatValue.of(input)
112+
return Optional.of(FloatValue.of(input))
104113
}
105114

106115
if (input is BigDecimal) {
107-
return FloatValue.newFloatValue(input).build()
116+
return Optional.of(FloatValue.newFloatValue(input).build())
108117
}
109118

110119
if (input is BigInteger) {
111-
return IntValue.newIntValue(input).build()
120+
return Optional.of(IntValue.newIntValue(input).build())
112121
}
113122

114123
if (input is Int) {
115-
return IntValue.of(input)
124+
return Optional.of(IntValue.of(input))
116125
}
117126

118127
if (input is Number) {
119-
return IntValue.newIntValue(BigInteger.valueOf(input.toLong())).build()
128+
return Optional.of(IntValue.newIntValue(BigInteger.valueOf(input.toLong())).build())
120129
}
121130

122131
if (input is Boolean) {
123-
return BooleanValue.of(input)
132+
return Optional.of(BooleanValue.of(input))
124133
}
125134

126135
if (input is Enum<*>) {
127-
return EnumValue.newEnumValue(input.name).build()
136+
return Optional.of(EnumValue.newEnumValue(input.name).build())
128137
}
129138

130139
if (input is Collection<*>) {
131-
return ArrayValue.newArrayValue()
132-
.values(input.map { toValue(it) })
133-
.build()
140+
return Optional.of(
141+
ArrayValue.newArrayValue()
142+
.values(input.map { toValue(it) })
143+
.build()
144+
)
134145
}
135146

136147
if (input is Map<*, *>) {
137-
return ObjectValue.newObjectValue()
138-
.objectFields(input.map { (key, value) -> ObjectField(key.toString(), toValue(value)) })
139-
.build()
148+
return Optional.of(
149+
ObjectValue.newObjectValue()
150+
.objectFields(input.map { (key, value) -> ObjectField(key.toString(), toValue(value)) })
151+
.build()
152+
)
140153
}
141154

142155
if (input is InputValue) {
143-
return ObjectValue.newObjectValue()
144-
.objectFields(input.inputValues().map { (name, value) -> ObjectField(name, toValue(value)) })
145-
.build()
156+
return Optional.of(
157+
ObjectValue.newObjectValue()
158+
.objectFields(input.inputValues().map { (name, value) -> ObjectField(name, toValue(value)) })
159+
.build()
160+
)
146161
}
147162

148-
val classes = sequenceOf(input::class) + input::class.allSuperclasses.asSequence() - Any::class
163+
return Optional.empty()
164+
}
165+
166+
protected fun getPropertyValues(
167+
classes: Sequence<KClass<out Any>>,
168+
input: Any?
169+
): MutableMap<String, Any?> {
149170
val propertyValues = mutableMapOf<String, Any?>()
150171

151172
for (klass in classes) {
@@ -158,13 +179,6 @@ class InputValueSerializer(private val scalars: Map<Class<*>, Coercing<*, *>> =
158179
propertyValues[property.name] = property.call(input)
159180
}
160181
}
161-
162-
val objectFields = propertyValues.asSequence()
163-
.filter { (_, value) -> value != null }
164-
.map { (name, value) -> ObjectField(name, toValue(value)) }
165-
.toList()
166-
return ObjectValue.newObjectValue()
167-
.objectFields(objectFields)
168-
.build()
182+
return propertyValues
169183
}
170184
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
*
3+
* Copyright 2020 Netflix, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package com.netflix.graphql.dgs.client.codegen
20+
21+
import graphql.language.Value
22+
23+
/**
24+
* Marks this property invisible for input value serialization.
25+
*/
26+
@Target(AnnotationTarget.PROPERTY)
27+
internal annotation class Transient
28+
29+
interface InputValueSerializerInterface {
30+
fun serialize(input: Any?): String
31+
fun toValue(input: Any?): Value<*>
32+
}
33+
34+
interface InputValue {
35+
fun inputValues(): List<Pair<String, Any?>>
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2021 Netflix, 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+
* http://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.netflix.graphql.dgs.client.codegen
18+
19+
import graphql.language.NullValue
20+
import graphql.language.ObjectField
21+
import graphql.language.ObjectValue
22+
import graphql.language.Value
23+
import graphql.schema.Coercing
24+
import kotlin.reflect.full.allSuperclasses
25+
26+
class NullableInputValueSerializer(scalars: Map<Class<*>, Coercing<*, *>> = emptyMap()) :
27+
InputValueSerializer(scalars) {
28+
29+
override fun toValue(input: Any?): Value<*> {
30+
if (input == null) {
31+
return NullValue.newNullValue().build()
32+
}
33+
34+
val optionalValue = getOptionalValue(input)
35+
36+
if (optionalValue.isPresent) {
37+
return optionalValue.get()
38+
}
39+
40+
val classes = (sequenceOf(input::class) + input::class.allSuperclasses.asSequence()) - Any::class
41+
val propertyValues = getPropertyValues(classes, input)
42+
43+
val objectFields = propertyValues.asSequence()
44+
.map { (name, value) -> ObjectField(name, toValue(value)) }
45+
.toList()
46+
return ObjectValue.newObjectValue()
47+
.objectFields(objectFields)
48+
.build()
49+
}
50+
}

graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/ProjectionSerializer.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import graphql.language.InlineFragment
2323
import graphql.language.SelectionSet
2424
import graphql.language.TypeName
2525

26-
class ProjectionSerializer(private val inputValueSerializer: InputValueSerializer) {
26+
class ProjectionSerializer(private val inputValueSerializer: InputValueSerializerInterface) {
2727

2828
fun toSelectionSet(projection: BaseProjectionNode): SelectionSet {
2929
val selectionSet = SelectionSet.newSelectionSet()

graphql-dgs-codegen-shared-core/src/test/kotlin/com/netflix/graphql/dgs/client/codegen/InputValueSerializerTest.kt

+20-4
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,27 @@ class InputValueSerializerTest {
6060
}
6161

6262
@Test
63-
fun `Null values should be skipped`() {
64-
val movieInput = MovieInput(1)
63+
fun `Null values should be serialized except when properties of a POJO`() {
64+
class ExamplePojo {
65+
private val movieId: String? = null
66+
private val movieTitle: String = "Bojack Horseman"
67+
}
6568

66-
val serialize = InputValueSerializer(mapOf(DateRange::class.java to DateRangeScalar())).serialize(movieInput)
67-
assertThat(serialize).isEqualTo("{movieId : 1}")
69+
assertThat(InputValueSerializer(mapOf()).serialize(null)).isEqualTo("null")
70+
assertThat(InputValueSerializer(mapOf()).serialize(mapOf("hello" to null))).isEqualTo("{hello : null}")
71+
assertThat(InputValueSerializer(mapOf()).serialize(ExamplePojo())).isEqualTo("{movieTitle : \"Bojack Horseman\"}")
72+
}
73+
74+
@Test
75+
fun `NullableInputValueSerializer allows null values from POJO`() {
76+
class ExamplePojo {
77+
private val movieId: String? = null
78+
private val movieTitle: String = "Bojack Horseman"
79+
}
80+
81+
assertThat(NullableInputValueSerializer(mapOf()).serialize(null)).isEqualTo("null")
82+
assertThat(NullableInputValueSerializer(mapOf()).serialize(mapOf("hello" to null))).isEqualTo("{hello : null}")
83+
assertThat(NullableInputValueSerializer(mapOf()).serialize(ExamplePojo())).isEqualTo("{movieId : null, movieTitle : \"Bojack Horseman\"}")
6884
}
6985

7086
@Test

0 commit comments

Comments
 (0)