Skip to content

Commit be9a215

Browse files
committed
Polishing GraphQlArgumentBinder
See gh-349
1 parent 16b1f94 commit be9a215

File tree

2 files changed

+61
-59
lines changed

2 files changed

+61
-59
lines changed

spring-graphql-docs/src/docs/asciidoc/index.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1406,7 +1406,7 @@ For example:
14061406
14071407
@MutationMapping
14081408
public void addBook(ArgumentValue<BookInput> bookInput) {
1409-
if (!bookInput.isOmitted) {
1409+
if (!bookInput.isOmitted()) {
14101410
BookInput value = bookInput.value();
14111411
// ...
14121412
}

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

+60-58
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,26 @@
4848

4949

5050
/**
51-
* Bind a GraphQL argument, or the full arguments map, onto a target object.
51+
* Binder that instantiates and populates a target Object to reflect the
52+
* complete structure of the {@link DataFetchingEnvironment#getArguments()
53+
* GraphQL arguments} input map.
5254
*
53-
* <p>Complex objects (non-scalar) are initialized either through the primary
54-
* data constructor where arguments are matched to constructor parameters, or
55-
* through the default constructor where arguments are matched to setter
56-
* property methods. In case objects are related to other objects, binding is
57-
* applied recursively to create nested objects.
55+
* <p>The input map is navigated recursively to create the full structure of
56+
* the target type. Objects in the target type are created either through a
57+
* primary, data constructor, in which case arguments are matched to constructor
58+
* parameters by name, or through the default constructor, in which case
59+
* arguments are matched to properties. Scalar values are converted, if
60+
* necessary, through a {@link ConversionService}.
5861
*
59-
* <p>Scalar values are converted to the expected target type through a
60-
* {@link ConversionService}, if provided.
62+
* <p>The binder does not stop at the first error, but rather accumulates as
63+
* many errors as it can in a {@link org.springframework.validation.BindingResult}.
64+
* At the end it raises a {@link BindException} that contains all recorded
65+
* errors along with the path at which each error occurred.
6166
*
62-
* <p>In case of any errors, when creating objects or converting scalar values,
63-
* a {@link BindException} is raised that contains all errors recorded along
64-
* with the path at which the errors occurred.
67+
* <p>The binder supports {@link Optional} as a wrapper around any Object or
68+
* scalar value in the target Object structure. In addition, it also supports
69+
* {@link ArgumentValue} as a wrapper that indicates whether a given input
70+
* argument was omitted rather than set to the {@literal "null"} literal.
6571
*
6672
* @author Brian Clozel
6773
* @author Rossen Stoyanchev
@@ -103,18 +109,15 @@ public void addDataBinderInitializer(Consumer<DataBinder> consumer) {
103109

104110

105111
/**
106-
* Bind a single argument, or the full arguments map, onto an object of the
107-
* given target type.
112+
* Create and populate an Object of the given target type, from a single
113+
* GraphQL argument, or from the full GraphQL arguments map.
108114
* @param environment for access to the arguments
109-
* @param name the name of the argument to bind, or {@code null} to
110-
* use the full arguments map
115+
* @param name the name of an argument, or {@code null} to use the full map
111116
* @param targetType the type of Object to create
112-
* @return the created Object, possibly {@code null}
113-
* @throws BindException in case of binding issues such as conversion errors,
114-
* mismatches between the source and the target object structure, and so on.
115-
* Binding issues are accumulated as {@link BindException#getFieldErrors()
116-
* field errors} where the {@link FieldError#getField() field} of each error
117-
* is the argument path where the issue occurred.
117+
* @return the created Object, possibly wrapped in {@link Optional} or in
118+
* {@link ArgumentValue}, or {@code null} if there is no value
119+
* @throws BindException containing one or more accumulated errors from
120+
* matching and/or converting arguments to the target Object
118121
*/
119122
@Nullable
120123
public Object bind(
@@ -137,21 +140,21 @@ public Object bind(
137140
}
138141

139142
/**
140-
* Bind the raw GraphQL argument value to an Object of the specified type.
141-
* @param name the name of a constructor parameter or a bean property of the
142-
* target Object that is to be initialized from the given raw value;
143-
* {@code "$"} if binding the top level Object; possibly indexed if binding
144-
* to a Collection element or to a Map value.
143+
* Create an Object from the given raw GraphQL argument value.
144+
* @param name the name of the constructor parameter or the property that
145+
* will be set from the returned value, possibly {@code "$"} for the top
146+
* Object, or an indexed property for a Collection element or Map value;
147+
* mainly for error recording, to keep track of the nested path
145148
* @param rawValue the raw argument value (Collection, Map, or scalar)
146-
* @param isOmitted whether the value with the given name was not provided
147-
* at all, as opposed to provided but set to the {@literal "null"} literal
149+
* @param isOmitted {@code true} if the argument was omitted from the input
150+
* and {@code false} if it was provided, but possibly {@code null}
148151
* @param targetType the type of Object to create
149-
* @param targetClass the resolved class from the targetType
150-
* @param bindingResult for keeping track of the nested path and errors
152+
* @param targetClass the target class, resolved from the targetType
153+
* @param bindingResult to accumulate errors
151154
* @return the target Object instance, possibly {@code null} if the source
152-
* value is {@code null} or if binding failed in which case the result will
153-
* contain errors; nevertheless we keep going to record as many errors as
154-
* we can accumulate
155+
* value is {@code null}, or if binding failed in which case the result will
156+
* contain errors; generally we keep going as far as we can and only raise
157+
* a {@link BindException} at the end to record as many errors as possible
155158
*/
156159
@SuppressWarnings({"ConstantConditions", "unchecked"})
157160
@Nullable
@@ -199,8 +202,8 @@ private Collection<?> bindCollection(
199202
ResolvableType elementType = collectionType.asCollection().getGeneric(0);
200203
Class<?> elementClass = collectionType.asCollection().getGeneric(0).resolve();
201204
if (elementClass == null) {
202-
bindingResult.rejectValue(null, "unknownType", "Unknown Collection element type");
203-
return Collections.emptyList(); // Keep going, report as many errors as we can
205+
bindingResult.rejectArgumentValue(name, null, "unknownType", "Unknown Collection element type");
206+
return Collections.emptyList(); // Keep going, to record more errors
204207
}
205208

206209
Collection<Object> collection =
@@ -228,9 +231,9 @@ private Object bindMap(
228231

229232
Constructor<?> constructor = BeanUtils.getResolvableConstructor(targetClass);
230233

231-
Object value = constructor.getParameterCount() > 0 ?
234+
Object value = (constructor.getParameterCount() > 0 ?
232235
bindMapToObjectViaConstructor(rawMap, constructor, targetType, bindingResult) :
233-
bindMapToObjectViaSetters(rawMap, constructor, targetType, bindingResult);
236+
bindMapToObjectViaSetters(rawMap, constructor, targetType, bindingResult));
234237

235238
bindingResult.popNestedPath();
236239

@@ -244,8 +247,8 @@ private Map<?, Object> bindMapToMap(
244247
ResolvableType valueType = targetType.asMap().getGeneric(1);
245248
Class<?> valueClass = valueType.resolve();
246249
if (valueClass == null) {
247-
bindingResult.rejectValue(null, "unknownType", "Unknown Map value type");
248-
return Collections.emptyMap(); // Keep going, report as many errors as we can
250+
bindingResult.rejectArgumentValue(name, null, "unknownType", "Unknown Map value type");
251+
return Collections.emptyMap(); // Keep going, to record more errors
249252
}
250253

251254
Map<String, Object> map = CollectionFactory.createMap(targetClass, rawMap.size());
@@ -261,28 +264,28 @@ private Map<?, Object> bindMapToMap(
261264

262265
@Nullable
263266
private Object bindMapToObjectViaConstructor(
264-
Map<String, Object> rawMap, Constructor<?> constructor, ResolvableType parentType,
267+
Map<String, Object> rawMap, Constructor<?> constructor, ResolvableType ownerType,
265268
ArgumentsBindingResult bindingResult) {
266269

267270
String[] paramNames = BeanUtils.getParameterNames(constructor);
268271
Class<?>[] paramTypes = constructor.getParameterTypes();
269-
Object[] args = new Object[paramTypes.length];
272+
Object[] constructorArguments = new Object[paramTypes.length];
270273

271274
for (int i = 0; i < paramNames.length; i++) {
272275
String name = paramNames[i];
273-
boolean isOmitted = !rawMap.containsKey(name);
274276

275277
ResolvableType targetType = ResolvableType.forType(
276-
ResolvableType.forConstructorParameter(constructor, i).getType(), parentType);
278+
ResolvableType.forConstructorParameter(constructor, i).getType(), ownerType);
277279

278-
args[i] = bindRawValue(name, rawMap.get(name), isOmitted, targetType, paramTypes[i], bindingResult);
280+
constructorArguments[i] = bindRawValue(
281+
name, rawMap.get(name), !rawMap.containsKey(name), targetType, paramTypes[i], bindingResult);
279282
}
280283

281284
try {
282-
return BeanUtils.instantiateClass(constructor, args);
285+
return BeanUtils.instantiateClass(constructor, constructorArguments);
283286
}
284287
catch (BeanInstantiationException ex) {
285-
// Ignore: we had binding errors to begin with
288+
// Ignore, if we had binding errors to begin with
286289
if (bindingResult.hasErrors()) {
287290
return null;
288291
}
@@ -291,7 +294,7 @@ private Object bindMapToObjectViaConstructor(
291294
}
292295

293296
private Object bindMapToObjectViaSetters(
294-
Map<String, Object> rawMap, Constructor<?> constructor, ResolvableType parentType,
297+
Map<String, Object> rawMap, Constructor<?> constructor, ResolvableType ownerType,
295298
ArgumentsBindingResult bindingResult) {
296299

297300
Object target = BeanUtils.instantiateClass(constructor);
@@ -306,7 +309,7 @@ private Object bindMapToObjectViaSetters(
306309
}
307310

308311
ResolvableType targetType =
309-
ResolvableType.forType(typeDescriptor.getResolvableType().getType(), parentType);
312+
ResolvableType.forType(typeDescriptor.getResolvableType().getType(), ownerType);
310313

311314
Object value = bindRawValue(
312315
key, entry.getValue(), false, targetType, typeDescriptor.getType(), bindingResult);
@@ -320,7 +323,7 @@ private Object bindMapToObjectViaSetters(
320323
// Ignore unknown property
321324
}
322325
catch (Exception ex) {
323-
bindingResult.rejectValue(value, "invalidPropertyValue", "Failed to set property value");
326+
bindingResult.rejectArgumentValue(key, value, "invalidPropertyValue", "Failed to set property value");
324327
}
325328
}
326329

@@ -339,22 +342,19 @@ private <T> T convertValue(
339342
(this.typeConverter != null ? this.typeConverter : new SimpleTypeConverter());
340343

341344
value = converter.convertIfNecessary(
342-
rawValue, (Class<?>) clazz,
343-
(type.getSource() instanceof MethodParameter param ? new TypeDescriptor(param) : null));
345+
rawValue, (Class<?>) clazz, new TypeDescriptor(type, null, null));
344346
}
345347
catch (TypeMismatchException ex) {
346-
bindingResult.pushNestedPath(name);
347-
bindingResult.rejectValue(rawValue, ex.getErrorCode(), "Failed to convert argument value");
348-
bindingResult.popNestedPath();
348+
bindingResult.rejectArgumentValue(name, rawValue, ex.getErrorCode(), "Failed to convert argument value");
349349
}
350350

351351
return (T) value;
352352
}
353353

354354

355355
/**
356-
* BindingResult without a target Object, only for keeping track of errors
357-
* and their associated, nested paths.
356+
* Subclass of {@link AbstractBindingResult} that doesn't have a target Object,
357+
* and takes the raw value as input when recording errors.
358358
*/
359359
@SuppressWarnings("serial")
360360
private static class ArgumentsBindingResult extends AbstractBindingResult {
@@ -379,9 +379,11 @@ protected Object getActualFieldValue(String field) {
379379
return null;
380380
}
381381

382-
public void rejectValue(@Nullable Object rawValue, String code, String defaultMessage) {
382+
public void rejectArgumentValue(
383+
String field, @Nullable Object rawValue, String code, String defaultMessage) {
384+
383385
addError(new FieldError(
384-
getObjectName(), fixedField(null), rawValue, true, resolveMessageCodes(code),
386+
getObjectName(), fixedField(field), rawValue, true, resolveMessageCodes(code),
385387
null, defaultMessage));
386388
}
387389
}

0 commit comments

Comments
 (0)