Skip to content

Add OptionalInput type to allow explicit null value detection #140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.springframework.graphql.data.method;

import javax.annotation.Nullable;
import java.util.Objects;

/**
* Wrapper used to represent optionally defined input arguments that allows us to distinguish between undefined value, explicit NULL value
* and specified value.
*/
public class OptionalInput<T> {

public static<T> OptionalInput<T> defined(@Nullable T value) {
return new OptionalInput.Defined<>(value);
}

@SuppressWarnings("unchecked")
public static<T> OptionalInput<T> undefined() {
return (OptionalInput<T>)new OptionalInput.Undefined();
}

/**
* Represents missing/undefined value.
*/
public static class Undefined extends OptionalInput<Void> {
@Override
public boolean equals(Object obj) {
return obj.getClass() == this.getClass();
}

@Override
public int hashCode() {
return super.hashCode();
}
}

/**
* Wrapper holding explicitly specified value including NULL.
*/
public static class Defined<T> extends OptionalInput<T> {
@Nullable
private final T value;

public Defined(@Nullable T value) {
this.value = value;
}

@Nullable
public T getValue() {
return value;
}

public Boolean isEmpty() {
return value == null;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Defined<?> defined = (Defined<?>) o;
if (isEmpty() == defined.isEmpty()) return true;
if (value == null) return false;
return value.equals(defined.value);
}

@Override
public int hashCode() {
return Objects.hash(value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
import org.springframework.beans.MutablePropertyValues;
import org.springframework.core.CollectionFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.Assert;
import org.springframework.graphql.data.method.OptionalInput;
import org.springframework.validation.DataBinder;

/**
Expand All @@ -42,6 +44,12 @@ class GraphQlArgumentInstantiator {

private final DataBinder converter = new DataBinder(null);

private final ConversionService conversionService = new OptionalInputArgumentConversionService();

GraphQlArgumentInstantiator() {
this.converter.setConversionService(this.conversionService);
}

/**
* Instantiate the given target type and bind data from
* {@link graphql.schema.DataFetchingEnvironment} arguments.
Expand All @@ -63,6 +71,7 @@ public <T> T instantiate(Map<String, Object> arguments, Class<T> targetType) {
MutablePropertyValues propertyValues = extractPropertyValues(arguments);
target = BeanUtils.instantiateClass(ctor);
DataBinder dataBinder = new DataBinder(target);
dataBinder.setConversionService(this.conversionService);
dataBinder.bind(propertyValues);
}
else {
Expand All @@ -74,10 +83,14 @@ public <T> T instantiate(Map<String, Object> arguments, Class<T> targetType) {
String paramName = paramNames[i];
Object value = arguments.get(paramName);
MethodParameter methodParam = new MethodParameter(ctor, i);
if (value == null && methodParam.isOptional()) {
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
if (value == null) {
if (methodParam.getParameterType() == OptionalInput.class) {
args[i] = arguments.containsKey(paramName) ? OptionalInput.defined(null) : OptionalInput.undefined();
} else if(methodParam.isOptional()) {
args[i] = (methodParam.getParameterType() == Optional.class && arguments.containsKey(paramName) ? Optional.empty() : null);
}
}
else if (value != null && CollectionFactory.isApproximableCollectionType(value.getClass())) {
else if (CollectionFactory.isApproximableCollectionType(value.getClass())) {
TypeDescriptor typeDescriptor = new TypeDescriptor(methodParam);
Class<?> elementType = typeDescriptor.getElementTypeDescriptor().getType();
args[i] = instantiateCollection(elementType, (Collection<Object>) value);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.springframework.graphql.data.method.annotation.support;

import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.graphql.data.method.OptionalInput;

import javax.swing.text.html.Option;
import java.util.Optional;

public class OptionalInputArgumentConversionService implements ConversionService {
@Override
public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
return false;
}

@Override
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
return targetType.getType() == OptionalInput.class || targetType.getType() == Optional.class;
}

@Override
public <T> T convert(Object source, Class<T> targetType) {
return null;
}

@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (targetType.getType() == Optional.class) {
return Optional.of(source);
} else {
return OptionalInput.defined(source);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
Expand All @@ -31,12 +32,15 @@
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.graphql.Book;
import org.springframework.graphql.data.method.OptionalInput;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;

import javax.annotation.Nullable;

import static org.assertj.core.api.Assertions.assertThat;

/**
Expand Down Expand Up @@ -83,7 +87,78 @@ void shouldResolveJavaBeanArgument() throws Exception {
Object result = resolver.resolveArgument(methodParameter, environment);
assertThat(result).isNotNull().isInstanceOf(BookInput.class);
assertThat((BookInput) result).hasFieldOrPropertyWithValue("name", "test name")
.hasFieldOrPropertyWithValue("authorId", 42L);
.hasFieldOrPropertyWithValue("authorId", 42L)
.hasFieldOrPropertyWithValue("notes", null);
}

@Test
void shouldResolveJavaBeanOptionalArgument() throws Exception {
Method addBook = ClassUtils.getMethod(BookController.class, "addBook", BookInput.class);
String payload = "{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42, \"notes\": \"Hello\"} }";
DataFetchingEnvironment environment = initEnvironment(payload);
MethodParameter methodParameter = getMethodParameter(addBook, 0);
Object result = resolver.resolveArgument(methodParameter, environment);
assertThat(result).isNotNull().isInstanceOf(BookInput.class);
assertThat((BookInput) result)
.hasFieldOrPropertyWithValue("name", "test name")
.hasFieldOrPropertyWithValue("authorId", 42L)
.hasFieldOrPropertyWithValue("notes", Optional.of("Hello"));
}

@Test
void shouldResolveJavaBeanOptionalNullArgument() throws Exception {
Method addBook = ClassUtils.getMethod(BookController.class, "addBook", BookInput.class);
String payload = "{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42, \"notes\": null} }";
DataFetchingEnvironment environment = initEnvironment(payload);
MethodParameter methodParameter = getMethodParameter(addBook, 0);
Object result = resolver.resolveArgument(methodParameter, environment);
assertThat(result).isNotNull().isInstanceOf(BookInput.class);
assertThat((BookInput) result)
.hasFieldOrPropertyWithValue("name", "test name")
.hasFieldOrPropertyWithValue("authorId", 42L)
.hasFieldOrPropertyWithValue("notes", Optional.empty());
}

@Test
void shouldResolveKotlinBeanArgument() throws Exception {
Method addBook = ClassUtils.getMethod(BookController.class, "ktAddBook", KotlinBookInput.class);
String payload = "{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42} }";
DataFetchingEnvironment environment = initEnvironment(payload);
MethodParameter methodParameter = getMethodParameter(addBook, 0);
Object result = resolver.resolveArgument(methodParameter, environment);
assertThat(result).isNotNull().isInstanceOf(KotlinBookInput.class);
assertThat((KotlinBookInput) result)
.hasFieldOrPropertyWithValue("name", "test name")
.hasFieldOrPropertyWithValue("authorId", 42L)
.hasFieldOrPropertyWithValue("notes", null);
}

@Test
void shouldResolveKotlinBeanOptionalArgument() throws Exception {
Method addBook = ClassUtils.getMethod(BookController.class, "ktAddBook", KotlinBookInput.class);
String payload = "{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42, \"notes\": \"Hello\"} }";
DataFetchingEnvironment environment = initEnvironment(payload);
MethodParameter methodParameter = getMethodParameter(addBook, 0);
Object result = resolver.resolveArgument(methodParameter, environment);
assertThat(result).isNotNull().isInstanceOf(KotlinBookInput.class);
assertThat((KotlinBookInput) result)
.hasFieldOrPropertyWithValue("name", "test name")
.hasFieldOrPropertyWithValue("authorId", 42L)
.hasFieldOrPropertyWithValue("notes", Optional.of("Hello"));
}

@Test
void shouldResolveKotlinBeanOptionalNullArgument() throws Exception {
Method addBook = ClassUtils.getMethod(BookController.class, "ktAddBook", KotlinBookInput.class);
String payload = "{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42, \"notes\": null} }";
DataFetchingEnvironment environment = initEnvironment(payload);
MethodParameter methodParameter = getMethodParameter(addBook, 0);
Object result = resolver.resolveArgument(methodParameter, environment);
assertThat(result).isNotNull().isInstanceOf(KotlinBookInput.class);
assertThat((KotlinBookInput) result)
.hasFieldOrPropertyWithValue("name", "test name")
.hasFieldOrPropertyWithValue("authorId", 42L)
.hasFieldOrPropertyWithValue("notes", Optional.empty());
}

@Test
Expand Down Expand Up @@ -127,6 +202,11 @@ public Book addBook(@Argument BookInput bookInput) {
return null;
}

@MutationMapping
public Book ktAddBook(@Argument KotlinBookInput bookInput) {
return null;
}

@MutationMapping
public List<Book> addBooks(@Argument List<Book> books) {
return null;
Expand All @@ -140,6 +220,9 @@ static class BookInput {

Long authorId;

@Nullable
Optional<String> notes = null;

public String getName() {
return this.name;
}
Expand All @@ -155,6 +238,15 @@ public Long getAuthorId() {
public void setAuthorId(Long authorId) {
this.authorId = authorId;
}

@Nullable
public Optional<String> getNotes() {
return this.notes;
}

public void setNotes(@Nullable Optional<String> notes) {
this.notes = notes;
}
}

static class Keyword {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.springframework.graphql.data.method.annotation.support;

import java.util.Optional

data class KotlinBookInput(
val name: String, val authorId: Long,
val notes: Optional<String?>?
)