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 7388491e..11508ec3 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 @@ -128,7 +128,7 @@ public class GraphQlArgumentBinder { try { // From Collection - if (CollectionFactory.isApproximableCollectionType(rawValue.getClass())) { + if (isApproximableCollectionType(rawValue)) { segments.push(argumentName); return createCollection((Collection) rawValue, targetType, bindingResult, segments); } @@ -164,6 +164,11 @@ 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 + } + @SuppressWarnings({"ConstantConditions", "unchecked"}) private Collection createCollection( Collection rawCollection, ResolvableType collectionType, @@ -253,7 +258,7 @@ public class GraphQlArgumentBinder { if (rawValue == null && methodParam.isOptional()) { args[i] = (paramTypes[i] == Optional.class ? Optional.empty() : null); } - else if (rawValue != null && CollectionFactory.isApproximableCollectionType(rawValue.getClass())) { + else if (rawValue != null && isApproximableCollectionType(rawValue)) { ResolvableType elementType = ResolvableType.forMethodParameter(methodParam); args[i] = createCollection((Collection) rawValue, elementType, bindingResult, segments); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/GraphQlArgumentBinderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java similarity index 87% rename from spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/GraphQlArgumentBinderTests.java rename to spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java index 00f9cb63..89949929 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/GraphQlArgumentBinderTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java @@ -14,8 +14,10 @@ * limitations under the License. */ -package org.springframework.graphql.data.method.annotation.support; +package org.springframework.graphql.data; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -45,14 +47,13 @@ class GraphQlArgumentBinderTests { private final ObjectMapper mapper = new ObjectMapper(); - private final ThreadLocal initializer = - ThreadLocal.withInitial(() -> new GraphQlArgumentBinder(null)); + private final GraphQlArgumentBinder binder = new GraphQlArgumentBinder(null); @Test void defaultConstructor() throws Exception { - Object result = initializer.get().bind( + Object result = this.binder.bind( environment("{\"key\":{\"name\":\"test\"}}"), "key", ResolvableType.forClass(SimpleBean.class)); @@ -63,7 +64,7 @@ class GraphQlArgumentBinderTests { @Test void defaultConstructorWithNestedBeanProperty() throws Exception { - Object result = initializer.get().bind( + Object result = this.binder.bind( environment( "{\"key\":{" + "\"name\":\"test name\"," + @@ -84,7 +85,7 @@ class GraphQlArgumentBinderTests { @Test void defaultConstructorWithNestedBeanListProperty() throws Exception { - Object result = initializer.get().bind( + Object result = this.binder.bind( environment("{\"key\":{\"items\":[{\"name\":\"first\"},{\"name\":\"second\"}]}}"), "key", ResolvableType.forClass(ItemListHolder.class)); @@ -96,7 +97,7 @@ class GraphQlArgumentBinderTests { @Test // gh-301 void defaultConstructorWithNestedBeanListEmpty() throws Exception { - Object result = initializer.get().bind( + Object result = this.binder.bind( environment("{\"key\":{\"items\": []}}"), "key", ResolvableType.forClass(ItemListHolder.class)); @@ -108,7 +109,7 @@ class GraphQlArgumentBinderTests { void defaultConstructorBindingError() { assertThatThrownBy( - () -> initializer.get().bind( + () -> this.binder.bind( environment("{\"key\":{\"name\":\"test\",\"age\":\"invalid\"}}"), "key", ResolvableType.forClass(SimpleBean.class))) .extracting(ex -> ((BindException) ex).getFieldErrors()) @@ -123,7 +124,7 @@ class GraphQlArgumentBinderTests { @Test void primaryConstructor() throws Exception { - Object result = initializer.get().bind( + Object result = this.binder.bind( environment("{\"key\":{\"name\":\"test\"}}"), "key", ResolvableType.forClass(PrimaryConstructorBean.class)); @@ -134,7 +135,7 @@ class GraphQlArgumentBinderTests { @Test void primaryConstructorWithBeanArgument() throws Exception { - Object result = initializer.get().bind( + Object result = this.binder.bind( environment( "{\"key\":{" + "\"item\":{\"name\":\"Item name\"}," + @@ -152,7 +153,7 @@ class GraphQlArgumentBinderTests { @Test void primaryConstructorWithNestedBeanList() throws Exception { - Object result = initializer.get().bind( + Object result = this.binder.bind( environment( "{\"key\":{\"items\":[" + "{\"name\":\"first\"}," + @@ -168,7 +169,7 @@ class GraphQlArgumentBinderTests { @Test void primaryConstructorNotFound() { assertThatThrownBy( - () -> initializer.get().bind( + () -> this.binder.bind( environment("{\"key\":{\"name\":\"test\"}}"), "key", ResolvableType.forClass(NoPrimaryConstructorBean.class))) .isInstanceOf(IllegalStateException.class) @@ -179,7 +180,7 @@ class GraphQlArgumentBinderTests { void primaryConstructorBindingError() { assertThatThrownBy( - () -> initializer.get().bind( + () -> this.binder.bind( environment( "{\"key\":{" + "\"name\":\"Hello\"," + @@ -205,7 +206,7 @@ class GraphQlArgumentBinderTests { void primaryConstructorBindingErrorWithNestedBeanList() { assertThatThrownBy( - () -> initializer.get().bind( + () -> this.binder.bind( environment( "{\"key\":{\"items\":[" + "{\"name\":\"first\", \"age\":\"invalid\"}," + @@ -225,6 +226,31 @@ class GraphQlArgumentBinderTests { }); } + @Test // gh-410 + void coercionWithSingletonList() throws Exception { + + Map itemMap = new HashMap<>(); + itemMap.put("name", "Joe"); + itemMap.put("age", "37"); + + Map arguments = new HashMap<>(); + arguments.put("key", Collections.singletonList(itemMap)); + + DataFetchingEnvironment environment = + DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(arguments).build(); + + Object result = this.binder.bind(environment, "key", + ResolvableType.forClassWithGenerics(List.class, Item.class)); + + assertThat(result).isNotNull().isInstanceOf(List.class); + List items = (List) result; + + assertThat(items).hasSize(1); + assertThat(items.get(0).getName()).isEqualTo("Joe"); + assertThat(items.get(0).getAge()).isEqualTo(37); + } + + @SuppressWarnings("unchecked") private DataFetchingEnvironment environment(String jsonPayload) throws JsonProcessingException { Map arguments = this.mapper.readValue(jsonPayload, Map.class);