Skip to content

Commit dfc4123

Browse files
committed
Add GraphQlArgumentBinder.NameResolver
It allows mapping GraphQL argument to Object property names. Closes gh-1146
1 parent aec5f22 commit dfc4123

File tree

2 files changed

+119
-16
lines changed

2 files changed

+119
-16
lines changed

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

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,16 @@ public class GraphQlArgumentBinder {
7979

8080
private final @Nullable SimpleTypeConverter typeConverter;
8181

82+
private final @Nullable NameResolver nameResolver;
83+
8284
private final boolean fallBackOnDirectFieldAccess;
8385

8486

8587
/**
8688
* Default constructor.
8789
*/
8890
public GraphQlArgumentBinder() {
89-
this((Options) null);
91+
this(Options.create());
9092
}
9193

9294
/**
@@ -96,7 +98,7 @@ public GraphQlArgumentBinder() {
9698
*/
9799
@Deprecated(since = "2.0", forRemoval = true)
98100
public GraphQlArgumentBinder(@Nullable ConversionService conversionService) {
99-
this(conversionService, false);
101+
this(Options.create().conversionService(conversionService));
100102
}
101103

102104
/**
@@ -107,13 +109,13 @@ public GraphQlArgumentBinder(@Nullable ConversionService conversionService) {
107109
*/
108110
@Deprecated(since = "2.0", forRemoval = true)
109111
public GraphQlArgumentBinder(@Nullable ConversionService service, boolean fallBackOnDirectFieldAccess) {
110-
this.typeConverter = initTypeConverter(service);
111-
this.fallBackOnDirectFieldAccess = fallBackOnDirectFieldAccess;
112+
this(Options.create().conversionService(service).fallBackOnDirectFieldAccess(fallBackOnDirectFieldAccess));
112113
}
113114

114-
public GraphQlArgumentBinder(@Nullable Options options) {
115-
this.typeConverter = ((options != null) ? initTypeConverter(options.conversionService()) : null);
116-
this.fallBackOnDirectFieldAccess = (options != null && options.fallBackOnDirectFieldAccess());
115+
public GraphQlArgumentBinder(Options options) {
116+
this.typeConverter = initTypeConverter(options.conversionService());
117+
this.nameResolver = options.nameResolver();
118+
this.fallBackOnDirectFieldAccess = options.fallBackOnDirectFieldAccess();
117119
}
118120

119121
private static @Nullable SimpleTypeConverter initTypeConverter(@Nullable ConversionService service) {
@@ -198,6 +200,10 @@ public GraphQlArgumentBinder(@Nullable Options options) {
198200
Assert.state(targetClass != null, "Could not resolve target type for: " + targetType);
199201
}
200202

203+
if (this.nameResolver != null) {
204+
name = this.nameResolver.resolveName(name);
205+
}
206+
201207
Object value;
202208
if (rawValue == null || targetClass == Object.class) {
203209
value = rawValue;
@@ -302,8 +308,21 @@ private Map<?, Object> bindMapToMap(
302308
ResolvableType targetType = ResolvableType.forType(
303309
ResolvableType.forConstructorParameter(constructor, i).getType(), ownerType);
304310

311+
Object rawValue = rawMap.get(name);
312+
boolean isNotPresent = !rawMap.containsKey(name);
313+
314+
if (rawValue == null && this.nameResolver != null) {
315+
for (String key : rawMap.keySet()) {
316+
if (this.nameResolver.resolveName(key).equals(name)) {
317+
rawValue = rawMap.get(key);
318+
isNotPresent = false;
319+
break;
320+
}
321+
}
322+
}
323+
305324
constructorArguments[i] = bindRawValue(
306-
name, rawMap.get(name), !rawMap.containsKey(name), targetType, paramTypes[i], bindingResult);
325+
name, rawValue, isNotPresent, targetType, paramTypes[i], bindingResult);
307326
}
308327

309328
Object target;
@@ -342,6 +361,9 @@ private void bindViaSetters(Object target,
342361

343362
for (Map.Entry<String, Object> entry : rawMap.entrySet()) {
344363
String key = entry.getKey();
364+
if (this.nameResolver != null) {
365+
key = this.nameResolver.resolveName(key);
366+
}
345367
TypeDescriptor typeDescriptor = beanWrapper.getPropertyTypeDescriptor(key);
346368
if (typeDescriptor == null && this.fallBackOnDirectFieldAccess) {
347369
Field field = ReflectionUtils.findField(beanWrapper.getWrappedClass(), key);
@@ -403,10 +425,15 @@ public static final class Options {
403425

404426
private final @Nullable ConversionService conversionService;
405427

428+
private final @Nullable NameResolver nameResolver;
429+
406430
private final boolean fallBackOnDirectFieldAccess;
407431

408-
private Options(@Nullable ConversionService conversionService, boolean fallBackOnDirectFieldAccess) {
432+
private Options(@Nullable ConversionService conversionService, @Nullable NameResolver nameResolver,
433+
boolean fallBackOnDirectFieldAccess) {
434+
409435
this.conversionService = conversionService;
436+
this.nameResolver = nameResolver;
410437
this.fallBackOnDirectFieldAccess = fallBackOnDirectFieldAccess;
411438
}
412439

@@ -416,7 +443,16 @@ private Options(@Nullable ConversionService conversionService, boolean fallBackO
416443
* @param service the service to use
417444
*/
418445
public Options conversionService(@Nullable ConversionService service) {
419-
return new Options(service, this.fallBackOnDirectFieldAccess);
446+
return new Options(service, this.nameResolver, this.fallBackOnDirectFieldAccess);
447+
}
448+
449+
/**
450+
* Add a resolver to help to map GraphQL argument names to Object property names.
451+
* @param resolver the resolver to add
452+
*/
453+
public Options nameResolver(NameResolver resolver) {
454+
resolver = ((this.nameResolver != null) ? this.nameResolver.andThen(resolver) : resolver);
455+
return new Options(this.conversionService, resolver, this.fallBackOnDirectFieldAccess);
420456
}
421457

422458
/**
@@ -427,13 +463,17 @@ public Options conversionService(@Nullable ConversionService service) {
427463
* @param fallBackOnDirectFieldAccess whether to fall back on direct field access
428464
*/
429465
public Options fallBackOnDirectFieldAccess(boolean fallBackOnDirectFieldAccess) {
430-
return new Options(this.conversionService, fallBackOnDirectFieldAccess);
466+
return new Options(this.conversionService, this.nameResolver, fallBackOnDirectFieldAccess);
431467
}
432468

433469
public @Nullable ConversionService conversionService() {
434470
return this.conversionService;
435471
}
436472

473+
public @Nullable NameResolver nameResolver() {
474+
return this.nameResolver;
475+
}
476+
437477
public boolean fallBackOnDirectFieldAccess() {
438478
return this.fallBackOnDirectFieldAccess;
439479
}
@@ -442,7 +482,33 @@ public boolean fallBackOnDirectFieldAccess() {
442482
* Create an instance without any options set.
443483
*/
444484
public static Options create() {
445-
return new Options(null, false);
485+
return new Options(null, (name) -> name, false);
486+
}
487+
}
488+
489+
490+
/**
491+
* Contract to customize the mapping of GraphQL argument names to Object
492+
* properties. This can be useful for dealing with naming conventions like
493+
* the use of "-" that cannot be used in Java property names.
494+
* @since 2.0.0
495+
*/
496+
public interface NameResolver {
497+
498+
/**
499+
* Resolve the given GraphQL argument name to an Object property name.
500+
* @param name the argument name
501+
* @return the resolved name to use
502+
*/
503+
String resolveName(String name);
504+
505+
/**
506+
* Append another resolver to be invoked after the current one.
507+
* @param resolver the resolver to invoked
508+
* @return a new composite resolver
509+
*/
510+
default NameResolver andThen(NameResolver resolver) {
511+
return (name) -> resolver.resolveName(resolveName(name));
446512
}
447513
}
448514

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

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ void dataBinding() throws Exception {
7070
assertThat(((SimpleBean) result).getName()).isEqualTo("test");
7171
}
7272

73+
@Test
74+
void dataBindingWithNameResolver() throws Exception {
75+
76+
GraphQlArgumentBinder.Options options = GraphQlArgumentBinder.Options.create()
77+
.nameResolver(name -> name.equals("name__") ? "name" : name);
78+
79+
Object result = bind(new GraphQlArgumentBinder(options),
80+
"{\"name__\":\"test\"}", ResolvableType.forClass(SimpleBean.class));
81+
82+
assertThat(result).isNotNull().isInstanceOf(SimpleBean.class);
83+
assertThat(((SimpleBean) result).getName()).isEqualTo("test");
84+
}
85+
7386
@Test
7487
void dataBindingWithNestedBeanProperty() throws Exception {
7588

@@ -123,8 +136,11 @@ void dataBindingWithNestedBeanListEmpty() throws Exception {
123136
@Test // gh-599
124137
void dataBindingWithDirectFieldAccess() throws Exception {
125138

126-
Object result = bind(
127-
new GraphQlArgumentBinder(new DefaultFormattingConversionService(), true /* fallBackOnFieldAccess */),
139+
GraphQlArgumentBinder.Options options = GraphQlArgumentBinder.Options.create()
140+
.conversionService(new DefaultFormattingConversionService())
141+
.fallBackOnDirectFieldAccess(true);
142+
143+
Object result = bind(new GraphQlArgumentBinder(options),
128144
"{\"items\":[{\"name\":\"first\"},{\"name\":\"second\"}]}",
129145
ResolvableType.forClass(DirectFieldAccessItemListHolder.class));
130146

@@ -221,6 +237,27 @@ void primaryConstructorWithBeanArgument() throws Exception {
221237
assertThat(itemBean.getAge()).isEqualTo(30);
222238
}
223239

240+
241+
@Test
242+
void primaryConstructorWithNameResolver() throws Exception {
243+
244+
GraphQlArgumentBinder.Options options = GraphQlArgumentBinder.Options.create()
245+
.nameResolver(name -> name.equals("age__") ? "age" : name);
246+
247+
Object result = bind(
248+
new GraphQlArgumentBinder(options),
249+
"{\"name\":\"Hello\",\"age__\":\"1\",\"item\":{\"name\":\"Item name\",\"age__\":\"2\"}}",
250+
ResolvableType.forClass(PrimaryConstructorItemBean.class));
251+
252+
assertThat(result).isNotNull().isInstanceOf(PrimaryConstructorItemBean.class);
253+
PrimaryConstructorItemBean itemBean = (PrimaryConstructorItemBean) result;
254+
255+
assertThat(itemBean.getName()).isEqualTo("Hello");
256+
assertThat(itemBean.getAge()).isEqualTo(1);
257+
assertThat(itemBean.getItem().getName()).isEqualTo("Item name");
258+
assertThat(itemBean.getItem().getAge()).isEqualTo(2);
259+
}
260+
224261
@Test
225262
void primaryConstructorWithOptionalBeanArgument() throws Exception {
226263

@@ -242,14 +279,14 @@ void primaryConstructorWithOptionalArgumentBeanArgument() throws Exception {
242279
ResolvableType.forClass(PrimaryConstructorOptionalArgumentItemBean.class);
243280

244281
Object result = bind(
245-
"{\"item\":{\"name\":\"Item name\",\"age\":\"30\"},\"name\":\"Hello\"}", targetType);
282+
"{\"name\":\"Hello\",\"item\":{\"name\":\"Item name\",\"age\":\"30\"}}", targetType);
246283

247284
assertThat(result).isInstanceOf(PrimaryConstructorOptionalArgumentItemBean.class).isNotNull();
248285
PrimaryConstructorOptionalArgumentItemBean itemBean = (PrimaryConstructorOptionalArgumentItemBean) result;
249286

287+
assertThat(itemBean.getName().value()).isEqualTo("Hello");
250288
assertThat(itemBean.getItem().value().getName()).isEqualTo("Item name");
251289
assertThat(itemBean.getItem().value().getAge()).isEqualTo(30);
252-
assertThat(itemBean.getName().value()).isEqualTo("Hello");
253290

254291
result = bind("{\"key\":{}}", targetType);
255292
itemBean = (PrimaryConstructorOptionalArgumentItemBean) result;

0 commit comments

Comments
 (0)