Support binding to class with enum generic type

Closes gh-349
This commit is contained in:
rstoyanchev
2022-10-21 18:24:17 +01:00
parent 36501c1c28
commit 16b1f94e69
2 changed files with 107 additions and 15 deletions

View File

@@ -178,8 +178,8 @@ public class GraphQlArgumentBinder {
value = bindMap(name, (Map<String, Object>) rawValue, targetType, targetClass, bindingResult);
}
else {
value = (targetClass.isAssignableFrom(rawValue.getClass()) ?
rawValue : convertValue(name, rawValue, targetClass, bindingResult));
value = (!targetClass.isAssignableFrom(rawValue.getClass()) ?
convertValue(name, rawValue, targetType, targetClass, bindingResult) : rawValue);
}
if (isOptional) {
@@ -229,8 +229,8 @@ public class GraphQlArgumentBinder {
Constructor<?> constructor = BeanUtils.getResolvableConstructor(targetClass);
Object value = constructor.getParameterCount() > 0 ?
bindMapToObjectViaConstructor(rawMap, constructor, bindingResult) :
bindMapToObjectViaSetters(rawMap, constructor, bindingResult);
bindMapToObjectViaConstructor(rawMap, constructor, targetType, bindingResult) :
bindMapToObjectViaSetters(rawMap, constructor, targetType, bindingResult);
bindingResult.popNestedPath();
@@ -261,7 +261,8 @@ public class GraphQlArgumentBinder {
@Nullable
private Object bindMapToObjectViaConstructor(
Map<String, Object> rawMap, Constructor<?> constructor, ArgumentsBindingResult bindingResult) {
Map<String, Object> rawMap, Constructor<?> constructor, ResolvableType parentType,
ArgumentsBindingResult bindingResult) {
String[] paramNames = BeanUtils.getParameterNames(constructor);
Class<?>[] paramTypes = constructor.getParameterTypes();
@@ -270,8 +271,11 @@ public class GraphQlArgumentBinder {
for (int i = 0; i < paramNames.length; i++) {
String name = paramNames[i];
boolean isOmitted = !rawMap.containsKey(name);
ResolvableType paramType = ResolvableType.forConstructorParameter(constructor, i);
args[i] = bindRawValue(name, rawMap.get(name), isOmitted, paramType, paramTypes[i], bindingResult);
ResolvableType targetType = ResolvableType.forType(
ResolvableType.forConstructorParameter(constructor, i).getType(), parentType);
args[i] = bindRawValue(name, rawMap.get(name), isOmitted, targetType, paramTypes[i], bindingResult);
}
try {
@@ -287,20 +291,26 @@ public class GraphQlArgumentBinder {
}
private Object bindMapToObjectViaSetters(
Map<String, Object> rawMap, Constructor<?> constructor, ArgumentsBindingResult bindingResult) {
Map<String, Object> rawMap, Constructor<?> constructor, ResolvableType parentType,
ArgumentsBindingResult bindingResult) {
Object target = BeanUtils.instantiateClass(constructor);
BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(target);
for (Map.Entry<String, Object> entry : rawMap.entrySet()) {
String key = entry.getKey();
TypeDescriptor type = beanWrapper.getPropertyTypeDescriptor(key);
if (type == null) {
TypeDescriptor typeDescriptor = beanWrapper.getPropertyTypeDescriptor(key);
if (typeDescriptor == null) {
// Ignore unknown property
continue;
}
ResolvableType targetType =
ResolvableType.forType(typeDescriptor.getResolvableType().getType(), parentType);
Object value = bindRawValue(
key, entry.getValue(), false, type.getResolvableType(), type.getType(), bindingResult);
key, entry.getValue(), false, targetType, typeDescriptor.getType(), bindingResult);
try {
if (value != null) {
beanWrapper.setPropertyValue(key, value);
@@ -320,18 +330,24 @@ public class GraphQlArgumentBinder {
@SuppressWarnings("unchecked")
@Nullable
private <T> T convertValue(
String name, @Nullable Object rawValue, Class<T> type, ArgumentsBindingResult bindingResult) {
String name, @Nullable Object rawValue, ResolvableType type, Class<T> clazz,
ArgumentsBindingResult bindingResult) {
Object value = null;
try {
TypeConverter converter = (this.typeConverter != null ? this.typeConverter : new SimpleTypeConverter());
value = converter.convertIfNecessary(rawValue, (Class<?>) type);
TypeConverter converter =
(this.typeConverter != null ? this.typeConverter : new SimpleTypeConverter());
value = converter.convertIfNecessary(
rawValue, (Class<?>) clazz,
(type.getSource() instanceof MethodParameter param ? new TypeDescriptor(param) : null));
}
catch (TypeMismatchException ex) {
bindingResult.pushNestedPath(name);
bindingResult.rejectValue(rawValue, ex.getErrorCode(), "Failed to convert argument value");
bindingResult.popNestedPath();
}
return (T) value;
}

View File

@@ -16,6 +16,8 @@
package org.springframework.graphql.data;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -31,8 +33,11 @@ import graphql.schema.DataFetchingEnvironment;
import graphql.schema.DataFetchingEnvironmentImpl;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.graphql.Book;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.lang.Nullable;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
@@ -51,7 +56,7 @@ class GraphQlArgumentBinderTests {
private final ObjectMapper mapper = new ObjectMapper();
private final GraphQlArgumentBinder binder = new GraphQlArgumentBinder();
private final GraphQlArgumentBinder binder = new GraphQlArgumentBinder(new DefaultFormattingConversionService());
@Test
@@ -113,6 +118,24 @@ class GraphQlArgumentBinderTests {
assertThat(((ItemListHolder) result).getItems()).hasSize(0);
}
@Test // gh-349
void dataBindingToBeanWithEnumGenericType() throws Exception {
Map<String, Object> argumentMap =
Collections.singletonMap("filter", Collections.singletonMap("enums", Arrays.asList("ONE", "TWO")));
Method method = EnumController.class.getMethod("enums", EnumInput.class);
ResolvableType targetType = ResolvableType.forMethodParameter(new MethodParameter(method, 0));
Object result = this.binder.bind(
DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(argumentMap).build(),
"filter", targetType);
assertThat(result).isNotNull().isInstanceOf(EnumInput.class);
EnumInput<FancyEnum> input = (EnumInput<FancyEnum>) result;
assertThat(input.getEnums()).hasSize(2).containsExactly(FancyEnum.ONE, FancyEnum.TWO);
}
@Test // gh-280
void dataBindingBindingError() {
assertThatThrownBy(
@@ -329,6 +352,24 @@ class GraphQlArgumentBinderTests {
Collections.singletonMap("name", "second"));
}
@Test // gh-349
void primaryConstructorWithEnumGenericType() throws Exception {
Map<String, Object> argumentMap =
Collections.singletonMap("filter", Collections.singletonMap("enums", Arrays.asList("ONE", "TWO")));
Method method = EnumController.class.getMethod("enums", ConstructorEnumInput.class);
ResolvableType targetType = ResolvableType.forMethodParameter(new MethodParameter(method, 0));
Object result = this.binder.bind(
DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(argumentMap).build(),
"filter", targetType);
assertThat(result).isNotNull().isInstanceOf(ConstructorEnumInput.class);
ConstructorEnumInput<FancyEnum> input = (ConstructorEnumInput<FancyEnum>) result;
assertThat(input.enums()).hasSize(2).containsExactly(FancyEnum.ONE, FancyEnum.TWO);
}
@SuppressWarnings("unchecked")
@Nullable
private Object bind(String json, ResolvableType targetType) throws Exception {
@@ -570,4 +611,39 @@ class GraphQlArgumentBinderTests {
}
}
static class EnumController {
public List<FancyEnum> enums(@Argument EnumInput<FancyEnum> filter) {
return filter.getEnums();
}
public List<FancyEnum> enums(@Argument ConstructorEnumInput<FancyEnum> filter) {
return filter.enums();
}
}
enum FancyEnum {
ONE, TWO, THREE
}
static class EnumInput<E extends Enum<E>> {
private List<E> enums;
public List<E> getEnums() {
return enums;
}
public void setEnums(List<E> enums) {
this.enums = enums;
}
}
record ConstructorEnumInput<E extends Enum<E>>(List<E> enums) {
}
}