From 2f598ed00e3847ff1ce49778c31ef6026573c092 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 13 Sep 2022 10:19:16 +0100 Subject: [PATCH] Support Optional constructor arg for argument binding Closes gh-470 --- .../graphql/data/GraphQlArgumentBinder.java | 14 +++--- .../data/GraphQlArgumentBinderTests.java | 44 +++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java index ce63da99..73369f3a 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java @@ -188,9 +188,10 @@ public class GraphQlArgumentBinder { return (type.resolve(Object.class).equals(Optional.class) ? Optional.ofNullable(value) : value); } - private boolean isApproximableCollectionType(Object rawValue) { - return (CollectionFactory.isApproximableCollectionType(rawValue.getClass()) || - rawValue instanceof List); // it may be SingletonList + private boolean isApproximableCollectionType(@Nullable Object rawValue) { + return (rawValue != null && + (CollectionFactory.isApproximableCollectionType(rawValue.getClass()) || + rawValue instanceof List)); // it may be SingletonList } @SuppressWarnings({"ConstantConditions", "unchecked"}) @@ -283,12 +284,15 @@ public class GraphQlArgumentBinder { if (rawValue == null && methodParam.isOptional()) { args[i] = (paramTypes[i] == Optional.class ? Optional.empty() : null); } - else if (rawValue != null && isApproximableCollectionType(rawValue)) { + else if (isApproximableCollectionType(rawValue)) { ResolvableType elementType = ResolvableType.forMethodParameter(methodParam); args[i] = createCollection((Collection) rawValue, elementType, bindingResult, segments); } else if (rawValue instanceof Map) { - args[i] = createValueOrNull((Map) rawValue, paramTypes[i], bindingResult, segments); + boolean isOptional = (paramTypes[i] == Optional.class); + Class type = (isOptional ? methodParam.nestedIfOptional().getNestedParameterType() : paramTypes[i]); + Object value = createValueOrNull((Map) rawValue, type, bindingResult, segments); + args[i] = (isOptional ? Optional.ofNullable(value) : value); } else { args[i] = convertValue(rawValue, paramTypes[i], new TypeDescriptor(methodParam), bindingResult, segments); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java index 3e1390c2..ca9adef6 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -32,6 +33,7 @@ import graphql.schema.DataFetchingEnvironmentImpl; import org.junit.jupiter.api.Test; import org.springframework.core.ResolvableType; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.graphql.Book; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; @@ -182,6 +184,26 @@ class GraphQlArgumentBinderTests { assertThat(((PrimaryConstructorItemBean) result).getAge()).isEqualTo(30); } + @Test + void primaryConstructorWithOptionalBeanArgument() throws Exception { + + GraphQlArgumentBinder argumentBinder = + new GraphQlArgumentBinder(new DefaultFormattingConversionService()); + + Object result = argumentBinder.bind( + environment( + "{\"key\":{" + + "\"item\":{\"name\":\"Item name\"}," + + "\"name\":\"Hello\"," + + "\"age\":\"30\"}}"), + "key", + ResolvableType.forClass(PrimaryConstructorOptionalItemBean.class)); + + assertThat(result).isNotNull().isInstanceOf(PrimaryConstructorOptionalItemBean.class); + assertThat(((PrimaryConstructorOptionalItemBean) result).getItem().get().getName()).isEqualTo("Item name"); + assertThat(((PrimaryConstructorOptionalItemBean) result).getName().get()).isEqualTo("Hello"); + } + @Test void primaryConstructorWithNestedBeanList() throws Exception { @@ -390,6 +412,28 @@ class GraphQlArgumentBinderTests { } + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + static class PrimaryConstructorOptionalItemBean { + + private final Optional name; + + private final Optional item; + + public PrimaryConstructorOptionalItemBean(Optional name, Optional item) { + this.name = name; + this.item = item; + } + + public Optional getName() { + return this.name; + } + + public Optional getItem() { + return item; + } + } + + static class NoPrimaryConstructorBean { NoPrimaryConstructorBean(String name) {