Skip to content

Commit 16b1f94

Browse files
committed
Support binding to class with enum generic type
Closes gh-349
1 parent 36501c1 commit 16b1f94

File tree

2 files changed

+107
-15
lines changed

2 files changed

+107
-15
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java

+30-14
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ else if (rawValue instanceof Map) {
178178
value = bindMap(name, (Map<String, Object>) rawValue, targetType, targetClass, bindingResult);
179179
}
180180
else {
181-
value = (targetClass.isAssignableFrom(rawValue.getClass()) ?
182-
rawValue : convertValue(name, rawValue, targetClass, bindingResult));
181+
value = (!targetClass.isAssignableFrom(rawValue.getClass()) ?
182+
convertValue(name, rawValue, targetType, targetClass, bindingResult) : rawValue);
183183
}
184184

185185
if (isOptional) {
@@ -229,8 +229,8 @@ private Object bindMap(
229229
Constructor<?> constructor = BeanUtils.getResolvableConstructor(targetClass);
230230

231231
Object value = constructor.getParameterCount() > 0 ?
232-
bindMapToObjectViaConstructor(rawMap, constructor, bindingResult) :
233-
bindMapToObjectViaSetters(rawMap, constructor, bindingResult);
232+
bindMapToObjectViaConstructor(rawMap, constructor, targetType, bindingResult) :
233+
bindMapToObjectViaSetters(rawMap, constructor, targetType, bindingResult);
234234

235235
bindingResult.popNestedPath();
236236

@@ -261,7 +261,8 @@ private Map<?, Object> bindMapToMap(
261261

262262
@Nullable
263263
private Object bindMapToObjectViaConstructor(
264-
Map<String, Object> rawMap, Constructor<?> constructor, ArgumentsBindingResult bindingResult) {
264+
Map<String, Object> rawMap, Constructor<?> constructor, ResolvableType parentType,
265+
ArgumentsBindingResult bindingResult) {
265266

266267
String[] paramNames = BeanUtils.getParameterNames(constructor);
267268
Class<?>[] paramTypes = constructor.getParameterTypes();
@@ -270,8 +271,11 @@ private Object bindMapToObjectViaConstructor(
270271
for (int i = 0; i < paramNames.length; i++) {
271272
String name = paramNames[i];
272273
boolean isOmitted = !rawMap.containsKey(name);
273-
ResolvableType paramType = ResolvableType.forConstructorParameter(constructor, i);
274-
args[i] = bindRawValue(name, rawMap.get(name), isOmitted, paramType, paramTypes[i], bindingResult);
274+
275+
ResolvableType targetType = ResolvableType.forType(
276+
ResolvableType.forConstructorParameter(constructor, i).getType(), parentType);
277+
278+
args[i] = bindRawValue(name, rawMap.get(name), isOmitted, targetType, paramTypes[i], bindingResult);
275279
}
276280

277281
try {
@@ -287,20 +291,26 @@ private Object bindMapToObjectViaConstructor(
287291
}
288292

289293
private Object bindMapToObjectViaSetters(
290-
Map<String, Object> rawMap, Constructor<?> constructor, ArgumentsBindingResult bindingResult) {
294+
Map<String, Object> rawMap, Constructor<?> constructor, ResolvableType parentType,
295+
ArgumentsBindingResult bindingResult) {
291296

292297
Object target = BeanUtils.instantiateClass(constructor);
293298
BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(target);
294299

295300
for (Map.Entry<String, Object> entry : rawMap.entrySet()) {
296301
String key = entry.getKey();
297-
TypeDescriptor type = beanWrapper.getPropertyTypeDescriptor(key);
298-
if (type == null) {
302+
TypeDescriptor typeDescriptor = beanWrapper.getPropertyTypeDescriptor(key);
303+
if (typeDescriptor == null) {
299304
// Ignore unknown property
300305
continue;
301306
}
307+
308+
ResolvableType targetType =
309+
ResolvableType.forType(typeDescriptor.getResolvableType().getType(), parentType);
310+
302311
Object value = bindRawValue(
303-
key, entry.getValue(), false, type.getResolvableType(), type.getType(), bindingResult);
312+
key, entry.getValue(), false, targetType, typeDescriptor.getType(), bindingResult);
313+
304314
try {
305315
if (value != null) {
306316
beanWrapper.setPropertyValue(key, value);
@@ -320,18 +330,24 @@ private Object bindMapToObjectViaSetters(
320330
@SuppressWarnings("unchecked")
321331
@Nullable
322332
private <T> T convertValue(
323-
String name, @Nullable Object rawValue, Class<T> type, ArgumentsBindingResult bindingResult) {
333+
String name, @Nullable Object rawValue, ResolvableType type, Class<T> clazz,
334+
ArgumentsBindingResult bindingResult) {
324335

325336
Object value = null;
326337
try {
327-
TypeConverter converter = (this.typeConverter != null ? this.typeConverter : new SimpleTypeConverter());
328-
value = converter.convertIfNecessary(rawValue, (Class<?>) type);
338+
TypeConverter converter =
339+
(this.typeConverter != null ? this.typeConverter : new SimpleTypeConverter());
340+
341+
value = converter.convertIfNecessary(
342+
rawValue, (Class<?>) clazz,
343+
(type.getSource() instanceof MethodParameter param ? new TypeDescriptor(param) : null));
329344
}
330345
catch (TypeMismatchException ex) {
331346
bindingResult.pushNestedPath(name);
332347
bindingResult.rejectValue(rawValue, ex.getErrorCode(), "Failed to convert argument value");
333348
bindingResult.popNestedPath();
334349
}
350+
335351
return (T) value;
336352
}
337353

spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java

+77-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.graphql.data;
1818

19+
import java.lang.reflect.Method;
20+
import java.util.Arrays;
1921
import java.util.Collections;
2022
import java.util.HashMap;
2123
import java.util.List;
@@ -31,8 +33,11 @@
3133
import graphql.schema.DataFetchingEnvironmentImpl;
3234
import org.junit.jupiter.api.Test;
3335

36+
import org.springframework.core.MethodParameter;
3437
import org.springframework.core.ResolvableType;
38+
import org.springframework.format.support.DefaultFormattingConversionService;
3539
import org.springframework.graphql.Book;
40+
import org.springframework.graphql.data.method.annotation.Argument;
3641
import org.springframework.lang.Nullable;
3742
import org.springframework.validation.BindException;
3843
import org.springframework.validation.FieldError;
@@ -51,7 +56,7 @@ class GraphQlArgumentBinderTests {
5156

5257
private final ObjectMapper mapper = new ObjectMapper();
5358

54-
private final GraphQlArgumentBinder binder = new GraphQlArgumentBinder();
59+
private final GraphQlArgumentBinder binder = new GraphQlArgumentBinder(new DefaultFormattingConversionService());
5560

5661

5762
@Test
@@ -113,6 +118,24 @@ void dataBindingWithNestedBeanListEmpty() throws Exception {
113118
assertThat(((ItemListHolder) result).getItems()).hasSize(0);
114119
}
115120

121+
@Test // gh-349
122+
void dataBindingToBeanWithEnumGenericType() throws Exception {
123+
124+
Map<String, Object> argumentMap =
125+
Collections.singletonMap("filter", Collections.singletonMap("enums", Arrays.asList("ONE", "TWO")));
126+
127+
Method method = EnumController.class.getMethod("enums", EnumInput.class);
128+
ResolvableType targetType = ResolvableType.forMethodParameter(new MethodParameter(method, 0));
129+
130+
Object result = this.binder.bind(
131+
DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(argumentMap).build(),
132+
"filter", targetType);
133+
134+
assertThat(result).isNotNull().isInstanceOf(EnumInput.class);
135+
EnumInput<FancyEnum> input = (EnumInput<FancyEnum>) result;
136+
assertThat(input.getEnums()).hasSize(2).containsExactly(FancyEnum.ONE, FancyEnum.TWO);
137+
}
138+
116139
@Test // gh-280
117140
void dataBindingBindingError() {
118141
assertThatThrownBy(
@@ -329,6 +352,24 @@ void primaryConstructorWithGenericObject() throws Exception {
329352
Collections.singletonMap("name", "second"));
330353
}
331354

355+
@Test // gh-349
356+
void primaryConstructorWithEnumGenericType() throws Exception {
357+
358+
Map<String, Object> argumentMap =
359+
Collections.singletonMap("filter", Collections.singletonMap("enums", Arrays.asList("ONE", "TWO")));
360+
361+
Method method = EnumController.class.getMethod("enums", ConstructorEnumInput.class);
362+
ResolvableType targetType = ResolvableType.forMethodParameter(new MethodParameter(method, 0));
363+
364+
Object result = this.binder.bind(
365+
DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(argumentMap).build(),
366+
"filter", targetType);
367+
368+
assertThat(result).isNotNull().isInstanceOf(ConstructorEnumInput.class);
369+
ConstructorEnumInput<FancyEnum> input = (ConstructorEnumInput<FancyEnum>) result;
370+
assertThat(input.enums()).hasSize(2).containsExactly(FancyEnum.ONE, FancyEnum.TWO);
371+
}
372+
332373
@SuppressWarnings("unchecked")
333374
@Nullable
334375
private Object bind(String json, ResolvableType targetType) throws Exception {
@@ -570,4 +611,39 @@ public int hashCode() {
570611
}
571612
}
572613

614+
615+
static class EnumController {
616+
617+
public List<FancyEnum> enums(@Argument EnumInput<FancyEnum> filter) {
618+
return filter.getEnums();
619+
}
620+
621+
public List<FancyEnum> enums(@Argument ConstructorEnumInput<FancyEnum> filter) {
622+
return filter.enums();
623+
}
624+
}
625+
626+
627+
enum FancyEnum {
628+
ONE, TWO, THREE
629+
}
630+
631+
632+
static class EnumInput<E extends Enum<E>> {
633+
634+
private List<E> enums;
635+
636+
public List<E> getEnums() {
637+
return enums;
638+
}
639+
640+
public void setEnums(List<E> enums) {
641+
this.enums = enums;
642+
}
643+
}
644+
645+
646+
record ConstructorEnumInput<E extends Enum<E>>(List<E> enums) {
647+
}
648+
573649
}

0 commit comments

Comments
 (0)