Add support for checking if an argument was omitted
Closes gh-518
This commit is contained in:
@@ -1272,14 +1272,19 @@ Schema mapping handler methods can have any of the following method arguments:
|
||||
| For access to a named field argument bound to a higher-level, typed Object.
|
||||
See <<controllers-schema-mapping-argument>>.
|
||||
|
||||
| `@Arguments`
|
||||
| For access to all field arguments bound to a higher-level, typed Object.
|
||||
See <<controllers-schema-mapping-arguments>>.
|
||||
|
||||
| `@Argument Map<String, Object>`
|
||||
| For access to the raw map of arguments, where `@Argument` does not have a
|
||||
`name` attribute.
|
||||
|
||||
| `ArgumentValue`
|
||||
| For access to a named field argument bound to a higher-level, typed Object along
|
||||
with a flag to indicate if the input argument was omitted vs set to `null`.
|
||||
See <<controllers-schema-mapping-argument-value>>.
|
||||
|
||||
| `@Arguments`
|
||||
| For access to all field arguments bound to a higher-level, typed Object.
|
||||
See <<controllers-schema-mapping-arguments>>.
|
||||
|
||||
| `@Arguments Map<String, Object>`
|
||||
| For access to the raw map of arguments.
|
||||
|
||||
@@ -1330,7 +1335,6 @@ Schema mapping handler methods can return:
|
||||
For this to work, `AnnotatedControllerConfigurer` must be configured with an `Executor`.
|
||||
|
||||
|
||||
|
||||
[[controllers-schema-mapping-argument]]
|
||||
==== `@Argument`
|
||||
|
||||
@@ -1377,6 +1381,43 @@ You can use `@Argument` with a `Map<String, Object>` argument, to obtain the raw
|
||||
all argument values. The name attribute on `@Argument` must not be set.
|
||||
|
||||
|
||||
[[controllers-schema-mapping-argument-value]]
|
||||
==== `ArgumentValue`
|
||||
|
||||
By default, input arguments in GraphQL are nullable and optional, which means an argument
|
||||
can be set to the `null` literal, or not provided at all. This distinction is useful for
|
||||
partial updates with a mutation where the underlying data may also be, either set to
|
||||
`null` or not changed at all accordingly. When using <<controllers-schema-mapping-argument>>
|
||||
there is no way to make such a distinction, because you would get `null` or an empty
|
||||
`Optional` in both cases.
|
||||
|
||||
If you want to know not whether a value was not provided at all, you can declare an
|
||||
`ArgumentValue` method parameter, which is a simple container for the resulting value,
|
||||
along with a flag to indicate whether the input argument was omitted altogether. You
|
||||
can use this instead of `@Argument`, in which case the argument name is determined from
|
||||
the method parameter name, or together with `@Argument` to specify the argument name.
|
||||
|
||||
For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Controller
|
||||
public class BookController {
|
||||
|
||||
@MutationMapping
|
||||
public void addBook(ArgumentValue<BookInput> bookInput) {
|
||||
if (!bookInput.isOmitted) {
|
||||
BookInput value = bookInput.value();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
`ArgumentValue` is also supported as a field within the object structure of an `@Argument`
|
||||
method parameter, either initialized via a constructor argument or via a setter, including
|
||||
as a field of an object nested at any level below the top level object.
|
||||
|
||||
|
||||
[[controllers-schema-mapping-arguments]]
|
||||
==== `@Arguments`
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql.data;
|
||||
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Simple container for the value from binding a GraphQL argument to a higher
|
||||
* level Object, along with a flag to indicate whether the input argument was
|
||||
* omitted altogether, as opposed to provided but set to the {@literal "null"}
|
||||
* literal.
|
||||
*
|
||||
* <p>Supported in one of the following places:
|
||||
* <ul>
|
||||
* <li>On a controller method parameter, either instead of
|
||||
* {@link org.springframework.graphql.data.method.annotation.Argument @Argument}
|
||||
* in which case the argument name is determined from the method parameter name,
|
||||
* or together with {@code @Argument} to specify the argument name.
|
||||
* <li>As a field within the object structure of an {@code @Argument} method
|
||||
* parameter, either initialized via a constructor argument or a setter,
|
||||
* including as a field of an object nested at any level below the top level
|
||||
* object.
|
||||
* </ul>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @param <T> the type of value contained
|
||||
* @since 1.1
|
||||
* @see <a href="http://spec.graphql.org/October2021/#sec-Non-Null.Nullable-vs-Optional">Nullable vs Optional</a>
|
||||
*/
|
||||
public final class ArgumentValue<T> {
|
||||
|
||||
private static final ArgumentValue<?> OMITTED = new ArgumentValue<>(null, false);
|
||||
|
||||
|
||||
@Nullable
|
||||
private final T value;
|
||||
|
||||
private final boolean omitted;
|
||||
|
||||
|
||||
private ArgumentValue(@Nullable T value, boolean omitted) {
|
||||
this.value = value;
|
||||
this.omitted = omitted;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return {@code true} if a non-null value is present, and {@code false} otherwise.
|
||||
*/
|
||||
public boolean isPresent() {
|
||||
return (this.value != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the input value was omitted altogether from the
|
||||
* input, and {@code false} if it was provided, but possibly set to the
|
||||
* {@literal "null"} literal.
|
||||
*/
|
||||
public boolean isOmitted() {
|
||||
return this.omitted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contained value, or {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public T value() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contained value as a nullable {@link Optional}.
|
||||
*/
|
||||
public Optional<T> asOptional() {
|
||||
return Optional.ofNullable(this.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
// This covers OMITTED constant
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof ArgumentValue<?> otherValue)) {
|
||||
return false;
|
||||
}
|
||||
return ObjectUtils.nullSafeEquals(this.value, otherValue.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = ObjectUtils.nullSafeHashCode(this.value);
|
||||
result = 31 * result + Boolean.hashCode(this.omitted);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Static factory method for an argument value that was provided, even if
|
||||
* it was set to {@literal "null}.
|
||||
* @param value the value to hold in the instance
|
||||
*/
|
||||
public static <T> ArgumentValue<T> ofNullable(@Nullable T value) {
|
||||
return new ArgumentValue<>(value, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method for an argument value that was omitted.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> ArgumentValue<T> omitted() {
|
||||
return (ArgumentValue<T>) OMITTED;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -121,12 +121,13 @@ public class GraphQlArgumentBinder {
|
||||
DataFetchingEnvironment environment, @Nullable String name, ResolvableType targetType)
|
||||
throws BindException {
|
||||
|
||||
Object rawValue = (name != null ?
|
||||
environment.getArgument(name) : environment.getArguments());
|
||||
Object rawValue = (name != null ? environment.getArgument(name) : environment.getArguments());
|
||||
boolean isOmitted = (name != null && !environment.getArguments().containsKey(name));
|
||||
|
||||
ArgumentsBindingResult bindingResult = new ArgumentsBindingResult(targetType);
|
||||
|
||||
Object value = bindRawValue("$", rawValue, targetType, targetType.resolve(Object.class), bindingResult);
|
||||
Object value = bindRawValue(
|
||||
"$", rawValue, isOmitted, targetType, targetType.resolve(Object.class), bindingResult);
|
||||
|
||||
if (bindingResult.hasErrors()) {
|
||||
throw new BindException(bindingResult);
|
||||
@@ -142,6 +143,8 @@ public class GraphQlArgumentBinder {
|
||||
* {@code "$"} if binding the top level Object; possibly indexed if binding
|
||||
* to a Collection element or to a Map value.
|
||||
* @param rawValue the raw argument value (Collection, Map, or scalar)
|
||||
* @param isOmitted whether the value with the given name was not provided
|
||||
* at all, as opposed to provided but set to the {@literal "null"} literal
|
||||
* @param targetType the type of Object to create
|
||||
* @param targetClass the resolved class from the targetType
|
||||
* @param bindingResult for keeping track of the nested path and errors
|
||||
@@ -153,12 +156,13 @@ public class GraphQlArgumentBinder {
|
||||
@SuppressWarnings({"ConstantConditions", "unchecked"})
|
||||
@Nullable
|
||||
private Object bindRawValue(
|
||||
String name, @Nullable Object rawValue, ResolvableType targetType, Class<?> targetClass,
|
||||
ArgumentsBindingResult bindingResult) {
|
||||
String name, @Nullable Object rawValue, boolean isOmitted,
|
||||
ResolvableType targetType, Class<?> targetClass, ArgumentsBindingResult bindingResult) {
|
||||
|
||||
boolean isOptional = (targetClass == Optional.class);
|
||||
boolean isArgumentValue = (targetClass == ArgumentValue.class);
|
||||
|
||||
if (isOptional) {
|
||||
if (isOptional || isArgumentValue) {
|
||||
targetType = targetType.getNested(2);
|
||||
targetClass = targetType.resolve();
|
||||
}
|
||||
@@ -178,7 +182,14 @@ public class GraphQlArgumentBinder {
|
||||
rawValue : convertValue(name, rawValue, targetClass, bindingResult));
|
||||
}
|
||||
|
||||
return (isOptional ? Optional.ofNullable(value) : value);
|
||||
if (isOptional) {
|
||||
value = Optional.ofNullable(value);
|
||||
}
|
||||
else if (isArgumentValue) {
|
||||
value = (isOmitted ? ArgumentValue.omitted() : ArgumentValue.ofNullable(value));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private Collection<?> bindCollection(
|
||||
@@ -198,7 +209,7 @@ public class GraphQlArgumentBinder {
|
||||
int index = 0;
|
||||
for (Object rawValue : rawCollection) {
|
||||
String indexedName = name + "[" + index++ + "]";
|
||||
collection.add(bindRawValue(indexedName, rawValue, elementType, elementClass, bindingResult));
|
||||
collection.add(bindRawValue(indexedName, rawValue, false, elementType, elementClass, bindingResult));
|
||||
}
|
||||
|
||||
return collection;
|
||||
@@ -242,7 +253,7 @@ public class GraphQlArgumentBinder {
|
||||
for (Map.Entry<String, Object> entry : rawMap.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String indexedName = name + "[" + key + "]";
|
||||
map.put(key, bindRawValue(indexedName, entry.getValue(), valueType, valueClass, bindingResult));
|
||||
map.put(key, bindRawValue(indexedName, entry.getValue(), false, valueType, valueClass, bindingResult));
|
||||
}
|
||||
|
||||
return map;
|
||||
@@ -258,8 +269,9 @@ 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), paramType, paramTypes[i], bindingResult);
|
||||
args[i] = bindRawValue(name, rawMap.get(name), isOmitted, paramType, paramTypes[i], bindingResult);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -288,7 +300,7 @@ public class GraphQlArgumentBinder {
|
||||
continue;
|
||||
}
|
||||
Object value = bindRawValue(
|
||||
key, entry.getValue(), type.getResolvableType(), type.getType(), bindingResult);
|
||||
key, entry.getValue(), false, type.getResolvableType(), type.getType(), bindingResult);
|
||||
try {
|
||||
if (value != null) {
|
||||
beanWrapper.setPropertyValue(key, value);
|
||||
@@ -313,7 +325,7 @@ public class GraphQlArgumentBinder {
|
||||
Object value = null;
|
||||
try {
|
||||
TypeConverter converter = (this.typeConverter != null ? this.typeConverter : new SimpleTypeConverter());
|
||||
value = converter.convertIfNecessary(rawValue, (Class<?>) type, TypeDescriptor.valueOf(type));
|
||||
value = converter.convertIfNecessary(rawValue, (Class<?>) type);
|
||||
}
|
||||
catch (TypeMismatchException ex) {
|
||||
bindingResult.pushNestedPath(name);
|
||||
|
||||
@@ -19,6 +19,7 @@ import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.data.GraphQlArgumentBinder;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
@@ -26,14 +27,27 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Resolver for {@link Argument @Argument} annotated method parameters, obtained
|
||||
* via {@link DataFetchingEnvironment#getArgument(String)} and converted to the
|
||||
* declared type of the method parameter.
|
||||
* Resolver for a method parameter that is annotated with
|
||||
* {@link Argument @Argument}. The specified raw argument value is obtained via
|
||||
* {@link DataFetchingEnvironment#getArgument(String)} and bound to a higher
|
||||
* level object, via {@link GraphQlArgumentBinder}, to match the target method
|
||||
* parameter type.
|
||||
*
|
||||
* <p>This resolver also supports wrapping the target object with
|
||||
* {@link ArgumentValue} if the application wants to differentiate between an
|
||||
* input argument that was set to {@code null} vs not provided at all. When
|
||||
* this wrapper type is used, the annotation is optional, and the name of the
|
||||
* argument is derived from the method parameter name.
|
||||
*
|
||||
* <p>An {@link ArgumentValue} can also be nested within the object structure
|
||||
* of an {@link Argument @Argument}-annotated method parameter.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 1.0.0
|
||||
* @see Argument
|
||||
* @see org.springframework.graphql.data.method.annotation.Argument
|
||||
* @see org.springframework.graphql.data.method.annotation.Arguments
|
||||
* @see org.springframework.graphql.data.GraphQlArgumentBinder
|
||||
*/
|
||||
public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@@ -48,7 +62,8 @@ public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentReso
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return (parameter.getParameterAnnotation(Argument.class) != null);
|
||||
return (parameter.getParameterAnnotation(Argument.class) != null ||
|
||||
parameter.getParameterType() == ArgumentValue.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,15 +74,21 @@ public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentReso
|
||||
}
|
||||
|
||||
static String getArgumentName(MethodParameter parameter) {
|
||||
Argument annotation = parameter.getParameterAnnotation(Argument.class);
|
||||
Assert.state(annotation != null, "Expected @Argument annotation");
|
||||
if (StringUtils.hasText(annotation.name())) {
|
||||
return annotation.name();
|
||||
Argument argument = parameter.getParameterAnnotation(Argument.class);
|
||||
if (argument != null) {
|
||||
if (StringUtils.hasText(argument.name())) {
|
||||
return argument.name();
|
||||
}
|
||||
}
|
||||
else if (parameter.getParameterType() != ArgumentValue.class) {
|
||||
throw new IllegalStateException("Expected @Argument annotation");
|
||||
}
|
||||
|
||||
String parameterName = parameter.getParameterName();
|
||||
if (parameterName != null) {
|
||||
return parameterName;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
"Name for argument of type [" + parameter.getNestedParameterType().getName() +
|
||||
"] not specified, and parameter name information not found in class file either.");
|
||||
|
||||
@@ -25,12 +25,18 @@ import org.springframework.graphql.data.method.annotation.Arguments;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Analogous to {@link ArgumentMethodArgumentResolver} but resolving method
|
||||
* parameters annotated with {@link Arguments @Arguments} and binding with the
|
||||
* full {@link DataFetchingEnvironment#getArgument(String) arguments} map.
|
||||
* Resolver for a method parameter that is annotated with
|
||||
* {@link Arguments @Arguments}, similar to what
|
||||
* {@link ArgumentMethodArgumentResolver} does but using the full
|
||||
* full {@link DataFetchingEnvironment#getArgument(String) GraphQL arguments}
|
||||
* map as the source for binding to the target Object rather than a specific
|
||||
* argument value within it.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
* @see org.springframework.graphql.data.method.annotation.Arguments
|
||||
* @see org.springframework.graphql.data.method.annotation.Argument
|
||||
* @see org.springframework.graphql.data.GraphQlArgumentBinder
|
||||
*/
|
||||
public class ArgumentsMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class GraphQlArgumentBinderTests {
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
private final GraphQlArgumentBinder binder = new GraphQlArgumentBinder(null);
|
||||
private final GraphQlArgumentBinder binder = new GraphQlArgumentBinder();
|
||||
|
||||
|
||||
@Test
|
||||
@@ -204,6 +204,33 @@ class GraphQlArgumentBinderTests {
|
||||
assertThat(((PrimaryConstructorOptionalItemBean) result).getName().get()).isEqualTo("Hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void primaryConstructorWithOptionalArgumentBeanArgument() throws Exception {
|
||||
|
||||
ResolvableType targetType =
|
||||
ResolvableType.forClass(PrimaryConstructorOptionalArgumentItemBean.class);
|
||||
|
||||
PrimaryConstructorOptionalArgumentItemBean result =
|
||||
(PrimaryConstructorOptionalArgumentItemBean) this.binder.bind(
|
||||
environment(
|
||||
"{\"key\":{" +
|
||||
"\"item\":{\"name\":\"Item name\",\"age\":\"30\"}," +
|
||||
"\"name\":\"Hello\"}}"),
|
||||
"key", targetType);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getItem().value().getName()).isEqualTo("Item name");
|
||||
assertThat(result.getItem().value().getAge()).isEqualTo(30);
|
||||
assertThat(result.getName().value()).isEqualTo("Hello");
|
||||
|
||||
result = (PrimaryConstructorOptionalArgumentItemBean)
|
||||
this.binder.bind(environment("{\"key\":{}}"), "key", targetType);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getItem().isOmitted()).isFalse();
|
||||
assertThat(result.getName().isOmitted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void primaryConstructorWithNestedBeanList() throws Exception {
|
||||
|
||||
@@ -495,6 +522,27 @@ class GraphQlArgumentBinderTests {
|
||||
}
|
||||
|
||||
|
||||
static class PrimaryConstructorOptionalArgumentItemBean {
|
||||
|
||||
private final ArgumentValue<String> name;
|
||||
|
||||
private final ArgumentValue<Item> item;
|
||||
|
||||
public PrimaryConstructorOptionalArgumentItemBean(ArgumentValue<String> name, ArgumentValue<Item> item) {
|
||||
this.name = name;
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
public ArgumentValue<String> getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public ArgumentValue<Item> getItem() {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static class NoPrimaryConstructorBean {
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.data.GraphQlArgumentBinder;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
@@ -44,15 +45,15 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
|
||||
|
||||
|
||||
@Test
|
||||
void shouldSupportAnnotatedParameters() {
|
||||
MethodParameter methodParameter = methodParam(BookController.class, "bookById", Long.class);
|
||||
assertThat(this.resolver.supportsParameter(methodParameter)).isTrue();
|
||||
}
|
||||
void supportsParameter() {
|
||||
MethodParameter param = methodParam(BookController.class, "bookById", Long.class);
|
||||
assertThat(this.resolver.supportsParameter(param)).isTrue();
|
||||
|
||||
@Test
|
||||
void shouldNotSupportParametersWithoutAnnotation() {
|
||||
MethodParameter methodParameter = methodParam(BookController.class, "notSupported", String.class);
|
||||
assertThat(this.resolver.supportsParameter(methodParameter)).isFalse();
|
||||
param = methodParam(BookController.class, "addBook", ArgumentValue.class);
|
||||
assertThat(this.resolver.supportsParameter(param)).isTrue();
|
||||
|
||||
param = methodParam(BookController.class, "notSupported", String.class);
|
||||
assertThat(this.resolver.supportsParameter(param)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -75,6 +76,20 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
|
||||
.hasFieldOrPropertyWithValue("authorId", 42L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveJavaBeanArgumentWithWrapper() throws Exception {
|
||||
Object result = this.resolver.resolveArgument(
|
||||
methodParam(BookController.class, "addBook", ArgumentValue.class),
|
||||
environment("{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42} }"));
|
||||
|
||||
assertThat(result)
|
||||
.isNotNull()
|
||||
.isInstanceOf(ArgumentValue.class)
|
||||
.extracting(value -> ((ArgumentValue<?>) value).value())
|
||||
.hasFieldOrPropertyWithValue("name", "test name")
|
||||
.hasFieldOrPropertyWithValue("authorId", 42L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveListOfJavaBeansArgument() throws Exception {
|
||||
Object result = this.resolver.resolveArgument(
|
||||
@@ -117,6 +132,11 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
|
||||
return null;
|
||||
}
|
||||
|
||||
@MutationMapping
|
||||
public Book addBook(ArgumentValue<BookInput> bookInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@MutationMapping
|
||||
public List<Book> addBooks(@Argument List<Book> books) {
|
||||
return null;
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.data.GraphQlArgumentBinder;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.annotation.Arguments;
|
||||
@@ -58,9 +59,16 @@ class ArgumentsMethodArgumentResolverTests extends ArgumentResolverTestSupport {
|
||||
methodParam(BookController.class, "addBook", BookInput.class),
|
||||
environment("{\"name\":\"test name\", \"authorId\":42}"));
|
||||
|
||||
assertThat(result).isNotNull().isInstanceOf(BookInput.class)
|
||||
.hasFieldOrPropertyWithValue("name", "test name")
|
||||
.hasFieldOrPropertyWithValue("authorId", 42L);
|
||||
assertThat(result)
|
||||
.isNotNull()
|
||||
.isInstanceOf(BookInput.class)
|
||||
.satisfies(value -> {
|
||||
BookInput input = (BookInput) value;
|
||||
assertThat(input.getName().isPresent()).isTrue();
|
||||
assertThat(input.getName().isOmitted()).isFalse();
|
||||
assertThat(input.getName().value()).isEqualTo("test name");
|
||||
assertThat(input.getAuthorId()).isEqualTo(42L);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -81,15 +89,15 @@ class ArgumentsMethodArgumentResolverTests extends ArgumentResolverTestSupport {
|
||||
@SuppressWarnings({"NotNullFieldNotInitialized", "unused"})
|
||||
static class BookInput {
|
||||
|
||||
String name;
|
||||
ArgumentValue<String> name;
|
||||
|
||||
Long authorId;
|
||||
|
||||
public String getName() {
|
||||
public ArgumentValue<String> getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
public void setName(ArgumentValue<String> name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user