Skip to content

Commit

Permalink
feat: add NullableInputVariableSerializer (#557)
Browse files Browse the repository at this point in the history
* feat: add NullableInputVariableSerializer

* fix reference of interface for input value serializer

* refactor: internalize serialize assignment

* fix nullable params

* remove unnecessary condition

* fix constructors
  • Loading branch information
Cole Turner authored May 25, 2023
1 parent d91aac3 commit d468c72
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,27 @@ import graphql.schema.Coercing
class GraphQLQueryRequest @JvmOverloads constructor(
val query: GraphQLQuery,
val projection: BaseProjectionNode? = null,
scalars: Map<Class<*>, Coercing<*, *>>? = null
options: GraphQLQueryRequestOptions? = null
) {

private var selectionSet: SelectionSet? = null

@JvmOverloads constructor(query: GraphQLQuery, selectionSet: SelectionSet, scalars: Map<Class<*>, Coercing<*, *>>? = null) : this(query = query, scalars = scalars) {
constructor(query: GraphQLQuery, projection: BaseProjectionNode, scalars: Map<Class<*>, Coercing<*, *>>) : this(query = query, projection = projection, options = GraphQLQueryRequestOptions(scalars = scalars))
constructor(query: GraphQLQuery, selectionSet: SelectionSet, scalars: Map<Class<*>, Coercing<*, *>>? = null) : this(query = query, projection = null, options = GraphQLQueryRequestOptions(scalars = scalars ?: emptyMap())) {
this.selectionSet = selectionSet
}
class GraphQLQueryRequestOptions(val scalars: Map<Class<*>, Coercing<*, *>> = emptyMap()) {
// When enabled, input values that are derived from properties
// whose values are null will be serialized in the query request
val allowNullablePropertyInputValues = false
}

val inputValueSerializer =
if (options?.allowNullablePropertyInputValues == true) {
NullableInputValueSerializer(options.scalars)
} else {
InputValueSerializer(options?.scalars ?: emptyMap())
}

val inputValueSerializer = InputValueSerializer(scalars ?: emptyMap())
val projectionSerializer = ProjectionSerializer(inputValueSerializer)

fun serialize(): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,15 @@ import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.OffsetDateTime
import java.util.Currency
import java.util.Date
import java.util.TimeZone
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.full.allSuperclasses
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible

/**
* Marks this property invisible for input value serialization.
*/
@Target(AnnotationTarget.PROPERTY)
internal annotation class Transient

interface InputValue {
fun inputValues(): List<Pair<String, Any?>>
}

class InputValueSerializer(private val scalars: Map<Class<*>, Coercing<*, *>> = emptyMap()) {
open class InputValueSerializer(private val scalars: Map<Class<*>, Coercing<*, *>> = emptyMap()) :
InputValueSerializerInterface {
companion object {
private val toStringClasses = setOf(
String::class,
Expand All @@ -68,84 +58,115 @@ class InputValueSerializer(private val scalars: Map<Class<*>, Coercing<*, *>> =
)
}

fun serialize(input: Any?): String {
override fun serialize(input: Any?): String {
return AstPrinter.printAst(toValue(input))
}

fun toValue(input: Any?): Value<*> {
override fun toValue(input: Any?): Value<*> {
if (input == null) {
return NullValue.newNullValue().build()
}

val optionalValue = getOptionalValue(input)

if (optionalValue.isPresent) {
return optionalValue.get()
}

val classes = (sequenceOf(input::class) + input::class.allSuperclasses.asSequence()) - Any::class
val propertyValues = getPropertyValues(classes, input)

val objectFields = propertyValues.asSequence()
.filter { (_, value) -> value != null }
.map { (name, value) -> ObjectField(name, toValue(value)) }
.toList()
return ObjectValue.newObjectValue()
.objectFields(objectFields)
.build()
}

protected fun getOptionalValue(input: Any): Optional<Value<*>> {
if (input is Value<*>) {
return input
return Optional.of(input)
}

for (scalar in scalars.keys) {
if (input::class.java == scalar || scalar.isAssignableFrom(input::class.java)) {
return scalars[scalar]!!.valueToLiteral(input)
return Optional.of(scalars[scalar]!!.valueToLiteral(input))
}
}

if (input::class in toStringClasses) {
return StringValue.of(input.toString())
return Optional.of(StringValue.of(input.toString()))
}

if (input is String) {
return StringValue.of(input)
return Optional.of(StringValue.of(input))
}

if (input is Float) {
return FloatValue.of(input.toDouble())
return Optional.of(FloatValue.of(input.toDouble()))
}

if (input is Double) {
return FloatValue.of(input)
return Optional.of(FloatValue.of(input))
}

if (input is BigDecimal) {
return FloatValue.newFloatValue(input).build()
return Optional.of(FloatValue.newFloatValue(input).build())
}

if (input is BigInteger) {
return IntValue.newIntValue(input).build()
return Optional.of(IntValue.newIntValue(input).build())
}

if (input is Int) {
return IntValue.of(input)
return Optional.of(IntValue.of(input))
}

if (input is Number) {
return IntValue.newIntValue(BigInteger.valueOf(input.toLong())).build()
return Optional.of(IntValue.newIntValue(BigInteger.valueOf(input.toLong())).build())
}

if (input is Boolean) {
return BooleanValue.of(input)
return Optional.of(BooleanValue.of(input))
}

if (input is Enum<*>) {
return EnumValue.newEnumValue(input.name).build()
return Optional.of(EnumValue.newEnumValue(input.name).build())
}

if (input is Collection<*>) {
return ArrayValue.newArrayValue()
.values(input.map { toValue(it) })
.build()
return Optional.of(
ArrayValue.newArrayValue()
.values(input.map { toValue(it) })
.build()
)
}

if (input is Map<*, *>) {
return ObjectValue.newObjectValue()
.objectFields(input.map { (key, value) -> ObjectField(key.toString(), toValue(value)) })
.build()
return Optional.of(
ObjectValue.newObjectValue()
.objectFields(input.map { (key, value) -> ObjectField(key.toString(), toValue(value)) })
.build()
)
}

if (input is InputValue) {
return ObjectValue.newObjectValue()
.objectFields(input.inputValues().map { (name, value) -> ObjectField(name, toValue(value)) })
.build()
return Optional.of(
ObjectValue.newObjectValue()
.objectFields(input.inputValues().map { (name, value) -> ObjectField(name, toValue(value)) })
.build()
)
}

val classes = sequenceOf(input::class) + input::class.allSuperclasses.asSequence() - Any::class
return Optional.empty()
}

protected fun getPropertyValues(
classes: Sequence<KClass<out Any>>,
input: Any?
): MutableMap<String, Any?> {
val propertyValues = mutableMapOf<String, Any?>()

for (klass in classes) {
Expand All @@ -158,13 +179,6 @@ class InputValueSerializer(private val scalars: Map<Class<*>, Coercing<*, *>> =
propertyValues[property.name] = property.call(input)
}
}

val objectFields = propertyValues.asSequence()
.filter { (_, value) -> value != null }
.map { (name, value) -> ObjectField(name, toValue(value)) }
.toList()
return ObjectValue.newObjectValue()
.objectFields(objectFields)
.build()
return propertyValues
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
*
* Copyright 2020 Netflix, 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
*
* http://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.netflix.graphql.dgs.client.codegen

import graphql.language.Value

/**
* Marks this property invisible for input value serialization.
*/
@Target(AnnotationTarget.PROPERTY)
internal annotation class Transient

interface InputValueSerializerInterface {
fun serialize(input: Any?): String
fun toValue(input: Any?): Value<*>
}

interface InputValue {
fun inputValues(): List<Pair<String, Any?>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2021 Netflix, 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
*
* http://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.netflix.graphql.dgs.client.codegen

import graphql.language.NullValue
import graphql.language.ObjectField
import graphql.language.ObjectValue
import graphql.language.Value
import graphql.schema.Coercing
import kotlin.reflect.full.allSuperclasses

class NullableInputValueSerializer(scalars: Map<Class<*>, Coercing<*, *>> = emptyMap()) :
InputValueSerializer(scalars) {

override fun toValue(input: Any?): Value<*> {
if (input == null) {
return NullValue.newNullValue().build()
}

val optionalValue = getOptionalValue(input)

if (optionalValue.isPresent) {
return optionalValue.get()
}

val classes = (sequenceOf(input::class) + input::class.allSuperclasses.asSequence()) - Any::class
val propertyValues = getPropertyValues(classes, input)

val objectFields = propertyValues.asSequence()
.map { (name, value) -> ObjectField(name, toValue(value)) }
.toList()
return ObjectValue.newObjectValue()
.objectFields(objectFields)
.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import graphql.language.InlineFragment
import graphql.language.SelectionSet
import graphql.language.TypeName

class ProjectionSerializer(private val inputValueSerializer: InputValueSerializer) {
class ProjectionSerializer(private val inputValueSerializer: InputValueSerializerInterface) {

fun toSelectionSet(projection: BaseProjectionNode): SelectionSet {
val selectionSet = SelectionSet.newSelectionSet()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,27 @@ class InputValueSerializerTest {
}

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

val serialize = InputValueSerializer(mapOf(DateRange::class.java to DateRangeScalar())).serialize(movieInput)
assertThat(serialize).isEqualTo("{movieId : 1}")
assertThat(InputValueSerializer(mapOf()).serialize(null)).isEqualTo("null")
assertThat(InputValueSerializer(mapOf()).serialize(mapOf("hello" to null))).isEqualTo("{hello : null}")
assertThat(InputValueSerializer(mapOf()).serialize(ExamplePojo())).isEqualTo("{movieTitle : \"Bojack Horseman\"}")
}

@Test
fun `NullableInputValueSerializer allows null values from POJO`() {
class ExamplePojo {
private val movieId: String? = null
private val movieTitle: String = "Bojack Horseman"
}

assertThat(NullableInputValueSerializer(mapOf()).serialize(null)).isEqualTo("null")
assertThat(NullableInputValueSerializer(mapOf()).serialize(mapOf("hello" to null))).isEqualTo("{hello : null}")
assertThat(NullableInputValueSerializer(mapOf()).serialize(ExamplePojo())).isEqualTo("{movieId : null, movieTitle : \"Bojack Horseman\"}")
}

@Test
Expand Down

0 comments on commit d468c72

Please sign in to comment.