Polishing

See gh-280
This commit is contained in:
rstoyanchev
2022-03-22 12:33:49 +00:00
parent 0ecf921ce9
commit 7e43223031
12 changed files with 105 additions and 81 deletions

View File

@@ -1118,11 +1118,11 @@ Schema mapping handler methods can have any of the following method arguments:
| Method Argument | Description
| `@Argument`
| For access to a named field argument converted to a higher-level, typed Object.
| 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 converted to a higher-level, typed Object.
| For access to all field arguments bound to a higher-level, typed Object.
See <<controllers-schema-mapping-arguments>>.
| `@ProjectedPayload` Interface
@@ -1173,11 +1173,12 @@ In GraphQL Java, `DataFetchingEnvironment` provides access to a map of field-spe
argument values. The values can be simple scalar values (e.g. String, Long), a `Map` of
values for more complex input, or a `List` of values.
Use the `@Argument` annotation to inject a named field argument into a handler method.
The method parameter can be a higher-level, typed Object of any type. It is created and
initialized from the named field argument value(s), either matching them to single data
constructor parameters, or using the default constructor and then matching keys onto
Object properties through a `org.springframework.validation.DataBinder`:
Use the `@Argument` annotation to have an argument bound to a target object and
injected into the handler method. Binding is performed by mapping argument values to a
primary data constructor of the expected method parameter type, or by using a default
constructor to create the object and then map argument values to its properties. This is
repeated recursively, using all nested argument values and creating nested target objects
accordingly. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@@ -1204,6 +1205,9 @@ TIP: The `@Argument` annotation does not have a "required" flag, nor the option
specify a default value. Both of these can be specified at the GraphQL schema level and
are enforced by GraphQL Java.
If binding fails, a `BindException` is raised with binding issues accumulated as field
errors where the `field` of each error is the argument path where the issue occurred.
You can use `@Argument` on a `Map<String, Object>` argument, to obtain all argument
values. The name attribute on `@Argument` must not be set.

View File

@@ -48,15 +48,17 @@ import org.springframework.validation.FieldError;
/**
* Bind GraphQL arguments to higher level objects.
*
* <p>The target object may have
* Bind a GraphQL argument values to higher level objects.
*
* <p>Binding is performed by mapping argument values to a primary data
* constructor of the target Object, or by using a default constructor
* and mapping argument values to properties. This is applied recursively.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public class GraphQlArgumentInitializer {
public class GraphQlArgumentBinder {
@Nullable
private final SimpleTypeConverter typeConverter;
@@ -64,7 +66,11 @@ public class GraphQlArgumentInitializer {
private final BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();
public GraphQlArgumentInitializer(@Nullable ConversionService conversionService) {
public GraphQlArgumentBinder() {
this(null);
}
public GraphQlArgumentBinder(@Nullable ConversionService conversionService) {
if (conversionService != null) {
this.typeConverter = new SimpleTypeConverter();
this.typeConverter.setConversionService(conversionService);
@@ -87,24 +93,27 @@ public class GraphQlArgumentInitializer {
/**
* Initialize an Object of the given {@code targetType}, either from a named
* {@link DataFetchingEnvironment#getArgument(String) argument value}, or from all
* {@link DataFetchingEnvironment#getArguments() values} as the source.
* @param environment the environment with the argument values
* @param name optionally, the name of an argument to initialize from,
* or if {@code null}, the full map of arguments is used.
* @param targetType the type of Object to initialize
* @return the initialized Object, or {@code null}
* @throws BindException raised in case of issues with binding argument values
* such as conversion errors, type mismatches between the source values and
* the target type structure, etc.
* Bind a single argument or the full arguments map onto an object of the
* given target type.
* @param environment to obtain the argument value(s) from
* @param argumentName the name of the argument to bind, or {@code null} to
* use the full arguments map
* @param targetType the type of Object to create
* @return the created Object, possibly {@code null}
* @throws BindException in case of binding issues such as conversion errors,
* mismatches between the source and the target object structure, and so on.
* Binding issues are accumulated as {@link BindException#getFieldErrors()
* field errors} where the {@code field} of each error is the argument path
* where the issue occurred.
*/
@Nullable
@SuppressWarnings("unchecked")
public Object initializeArgument(
DataFetchingEnvironment environment, @Nullable String name, ResolvableType targetType) throws BindException {
public Object bind(
DataFetchingEnvironment environment, @Nullable String argumentName, ResolvableType targetType)
throws BindException {
Object rawValue = (name != null ? environment.getArgument(name) : environment.getArguments());
Object rawValue = (argumentName != null ?
environment.getArgument(argumentName) : environment.getArguments());
if (rawValue == null) {
return wrapAsOptionalIfNecessary(null, targetType);
@@ -113,7 +122,7 @@ public class GraphQlArgumentInitializer {
Class<?> targetClass = targetType.resolve();
Assert.notNull(targetClass, "Could not determine target type from " + targetType);
DataBinder binder = new DataBinder(null, name != null ? name : "arguments");
DataBinder binder = new DataBinder(null, argumentName != null ? argumentName : "arguments");
BindingResult bindingResult = binder.getBindingResult();
Stack<String> segments = new Stack<>();
@@ -121,7 +130,7 @@ public class GraphQlArgumentInitializer {
// From Collection
if (CollectionFactory.isApproximableCollectionType(rawValue.getClass())) {
segments.push(name);
segments.push(argumentName);
return createCollection((Collection<Object>) rawValue, targetType, bindingResult, segments);
}

View File

@@ -22,20 +22,26 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.validation.BindException;
/**
* Annotation to bind a method parameter to a GraphQL input
* {@link graphql.schema.DataFetchingEnvironment#getArgument(String) argument}.
* Annotation to bind a named GraphQL
* {@link graphql.schema.DataFetchingEnvironment#getArgument(String) argument}
* onto a method parameter.
*
* <p>Binding is performed by mapping argument values to a primary data
* constructor of the expected method parameter type, or by using a default
* constructor to create it and then map values to its properties. This is
* applied recursively, using all nested values and creating nested target
* objects.
*
* <p>If binding fails, a {@link BindException} is raised with binding issues
* accumulated as {@link BindException#getFieldErrors() field errors} where the
* {@code field} of each error is the argument path where the issue occurred.
*
* <p>If the method parameter is {@link java.util.Map Map&lt;String, Object&gt;}
* and a parameter name is not specified, then the map parameter is populated
* via {@link graphql.schema.DataFetchingEnvironment#getArguments()}.
*
* <p>The target method parameter can be a higher-level, typed Object of any
* type. It is created and initialized from the named field argument value(s),
* either matching them to single data constructor parameters, or using the
* default constructor and then matching keys onto Object properties through
* a {@link org.springframework.validation.DataBinder}.
* and a parameter name is not specified, then the resolves value is the raw
* {@link graphql.schema.DataFetchingEnvironment#getArguments() arguments} map.
*
* <p>Note that this annotation has neither a "required" flag nor the option to
* specify a default value, both of which can be specified at the GraphQL schema

View File

@@ -21,13 +21,14 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import graphql.schema.DataFetchingEnvironment;
/**
* Similar to {@link Argument @Argument} but using the full map of argument
* values as the source of values to bind to the target Object.
* Analogous to {@link Argument} but binding with the full
* {@link DataFetchingEnvironment#getArgument(String) arguments} map.
*
* @author Rossen Stoyanchev
* @since 1.0.0
* @see Argument
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)

View File

@@ -53,7 +53,7 @@ import org.springframework.expression.BeanResolver;
import org.springframework.format.FormatterRegistrar;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.graphql.data.GraphQlArgumentInitializer;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.method.HandlerMethod;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
@@ -159,7 +159,7 @@ public class AnnotatedControllerConfigurer
this.argumentResolvers.addResolver(new ProjectedPayloadMethodArgumentResolver(obtainApplicationContext()));
}
this.argumentResolvers.addResolver(new ArgumentMapMethodArgumentResolver());
GraphQlArgumentInitializer initializer = new GraphQlArgumentInitializer(this.conversionService);
GraphQlArgumentBinder initializer = new GraphQlArgumentBinder(this.conversionService);
this.argumentResolvers.addResolver(new ArgumentMethodArgumentResolver(initializer));
this.argumentResolvers.addResolver(new ArgumentsMethodArgumentResolver(initializer));
this.argumentResolvers.addResolver(new ContextValueMethodArgumentResolver());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* 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.
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.data.method.annotation.support;
import java.util.Map;
@@ -24,9 +25,11 @@ import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.util.StringUtils;
/**
* Resolves {@link Map} method arguments annotated with an @{@link Argument}
* where the annotation does not specify an argument name.
* Resolves a {@link Map} method parameter annotated with an
* {@link Argument @Argument} by returning the GraphQL
* {@link DataFetchingEnvironment#getArguments() arguments} map.
*
* @author Rossen Stoyanchev
* @since 1.0.0

View File

@@ -19,7 +19,7 @@ import graphql.schema.DataFetchingEnvironment;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.graphql.data.GraphQlArgumentInitializer;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.util.Assert;
@@ -33,13 +33,14 @@ import org.springframework.util.StringUtils;
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 1.0.0
* @see Argument
*/
public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver {
private final GraphQlArgumentInitializer argumentInitializer;
private final GraphQlArgumentBinder argumentInitializer;
public ArgumentMethodArgumentResolver(GraphQlArgumentInitializer initializer) {
public ArgumentMethodArgumentResolver(GraphQlArgumentBinder initializer) {
Assert.notNull(initializer, "GraphQlArgumentInitializer is required");
this.argumentInitializer = initializer;
}
@@ -47,14 +48,14 @@ public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentReso
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(Argument.class) != null;
return (parameter.getParameterAnnotation(Argument.class) != null);
}
@Override
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
String name = getArgumentName(parameter);
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
return this.argumentInitializer.initializeArgument(environment, name, resolvableType);
return this.argumentInitializer.bind(environment, name, resolvableType);
}
static String getArgumentName(MethodParameter parameter) {

View File

@@ -19,25 +19,25 @@ import graphql.schema.DataFetchingEnvironment;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.graphql.data.GraphQlArgumentInitializer;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
import org.springframework.graphql.data.method.annotation.Arguments;
import org.springframework.util.Assert;
/**
* Resolver for {@link Arguments @Arguments} annotated method parameters,
* obtained via {@link DataFetchingEnvironment#getArgument(String)} and
* converted to the declared type of the method parameter.
* Analogous to {@link ArgumentMethodArgumentResolver} but resolving method
* parameters annotated with {@link Arguments @Arguments} and binding with the
* full {@link DataFetchingEnvironment#getArgument(String) arguments} map.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public class ArgumentsMethodArgumentResolver implements HandlerMethodArgumentResolver {
private final GraphQlArgumentInitializer argumentInitializer;
private final GraphQlArgumentBinder argumentInitializer;
public ArgumentsMethodArgumentResolver(GraphQlArgumentInitializer initializer) {
public ArgumentsMethodArgumentResolver(GraphQlArgumentBinder initializer) {
Assert.notNull(initializer, "GraphQlArgumentInitializer is required");
this.argumentInitializer = initializer;
}
@@ -51,7 +51,7 @@ public class ArgumentsMethodArgumentResolver implements HandlerMethodArgumentRes
@Override
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
return this.argumentInitializer.initializeArgument(environment, null, resolvableType);
return this.argumentInitializer.bind(environment, null, resolvableType);
}
}

View File

@@ -38,7 +38,7 @@ import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.graphql.data.GraphQlArgumentInitializer;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.util.Assert;
@@ -92,12 +92,12 @@ public abstract class QueryByExampleDataFetcher<T> {
private final TypeInformation<T> domainType;
private final GraphQlArgumentInitializer argumentInitializer;
private final GraphQlArgumentBinder argumentInitializer;
QueryByExampleDataFetcher(TypeInformation<T> domainType) {
this.domainType = domainType;
this.argumentInitializer = new GraphQlArgumentInitializer(null);
this.argumentInitializer = new GraphQlArgumentBinder();
}
@@ -109,7 +109,7 @@ public abstract class QueryByExampleDataFetcher<T> {
@SuppressWarnings({"ConstantConditions", "unchecked"})
protected Example<T> buildExample(DataFetchingEnvironment env) throws BindException {
ResolvableType targetType = ResolvableType.forClass(this.domainType.getType());
return (Example<T>) Example.of(this.argumentInitializer.initializeArgument(env, null, targetType));
return (Example<T>) Example.of(this.argumentInitializer.bind(env, null, targetType));
}
protected boolean requiresProjection(Class<?> resultType) {

View File

@@ -24,7 +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.GraphQlArgumentInitializer;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
@@ -40,7 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
private final HandlerMethodArgumentResolver resolver = new ArgumentMethodArgumentResolver(
new GraphQlArgumentInitializer(new DefaultFormattingConversionService()));
new GraphQlArgumentBinder(new DefaultFormattingConversionService()));
@Test

View File

@@ -22,7 +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.GraphQlArgumentInitializer;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
import org.springframework.graphql.data.method.annotation.Arguments;
import org.springframework.graphql.data.method.annotation.MutationMapping;
@@ -37,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class ArgumentsMethodArgumentResolverTests extends ArgumentResolverTestSupport {
private final HandlerMethodArgumentResolver resolver = new ArgumentsMethodArgumentResolver(
new GraphQlArgumentInitializer(new DefaultFormattingConversionService()));
new GraphQlArgumentBinder(new DefaultFormattingConversionService()));
@Test

View File

@@ -27,7 +27,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.ResolvableType;
import org.springframework.graphql.Book;
import org.springframework.graphql.data.GraphQlArgumentInitializer;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
@@ -36,22 +36,22 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link GraphQlArgumentInitializer}
* Tests for {@link GraphQlArgumentBinder}
*
* @author Brian Clozel
* @author Rossen Stoyanchev
*/
class GraphQlArgumentInitializerTests {
class GraphQlArgumentBinderTests {
private final ObjectMapper mapper = new ObjectMapper();
private final ThreadLocal<GraphQlArgumentInitializer> initializer = ThreadLocal.withInitial(() -> new GraphQlArgumentInitializer(null));
private final ThreadLocal<GraphQlArgumentBinder> initializer = ThreadLocal.withInitial(() -> new GraphQlArgumentBinder(null));
@Test
void defaultConstructor() throws Exception {
Object result = initializer.get().initializeArgument(
Object result = initializer.get().bind(
environment("{\"key\":{\"name\":\"test\"}}"), "key",
ResolvableType.forClass(SimpleBean.class));
@@ -62,7 +62,7 @@ class GraphQlArgumentInitializerTests {
@Test
void defaultConstructorWithNestedBeanProperty() throws Exception {
Object result = initializer.get().initializeArgument(
Object result = initializer.get().bind(
environment(
"{\"key\":{" +
"\"name\":\"test name\"," +
@@ -83,7 +83,7 @@ class GraphQlArgumentInitializerTests {
@Test
void defaultConstructorWithNestedBeanListProperty() throws Exception {
Object result = initializer.get().initializeArgument(
Object result = initializer.get().bind(
environment("{\"key\":{\"items\":[{\"name\":\"first\"},{\"name\":\"second\"}]}}"), "key",
ResolvableType.forClass(ItemListHolder.class));
@@ -95,7 +95,7 @@ class GraphQlArgumentInitializerTests {
@Test // gh-301
void defaultConstructorWithNestedBeanListEmpty() throws Exception {
Object result = initializer.get().initializeArgument(
Object result = initializer.get().bind(
environment("{\"key\":{\"items\": []}}"), "key",
ResolvableType.forClass(ItemListHolder.class));
@@ -107,7 +107,7 @@ class GraphQlArgumentInitializerTests {
void defaultConstructorBindingError() {
assertThatThrownBy(
() -> initializer.get().initializeArgument(
() -> initializer.get().bind(
environment("{\"key\":{\"name\":\"test\",\"age\":\"invalid\"}}"), "key",
ResolvableType.forClass(SimpleBean.class)))
.extracting(ex -> ((BindException) ex).getFieldErrors())
@@ -122,7 +122,7 @@ class GraphQlArgumentInitializerTests {
@Test
void primaryConstructor() throws Exception {
Object result = initializer.get().initializeArgument(
Object result = initializer.get().bind(
environment("{\"key\":{\"name\":\"test\"}}"), "key",
ResolvableType.forClass(PrimaryConstructorBean.class));
@@ -133,7 +133,7 @@ class GraphQlArgumentInitializerTests {
@Test
void primaryConstructorWithBeanArgument() throws Exception {
Object result = initializer.get().initializeArgument(
Object result = initializer.get().bind(
environment(
"{\"key\":{" +
"\"item\":{\"name\":\"Item name\"}," +
@@ -151,7 +151,7 @@ class GraphQlArgumentInitializerTests {
@Test
void primaryConstructorWithNestedBeanList() throws Exception {
Object result = initializer.get().initializeArgument(
Object result = initializer.get().bind(
environment(
"{\"key\":{\"items\":[" +
"{\"name\":\"first\"}," +
@@ -167,7 +167,7 @@ class GraphQlArgumentInitializerTests {
@Test
void primaryConstructorNotFound() {
assertThatThrownBy(
() -> initializer.get().initializeArgument(
() -> initializer.get().bind(
environment("{\"key\":{\"name\":\"test\"}}"), "key",
ResolvableType.forClass(NoPrimaryConstructorBean.class)))
.isInstanceOf(IllegalStateException.class)
@@ -178,7 +178,7 @@ class GraphQlArgumentInitializerTests {
void primaryConstructorBindingError() {
assertThatThrownBy(
() -> initializer.get().initializeArgument(
() -> initializer.get().bind(
environment(
"{\"key\":{" +
"\"name\":\"Hello\"," +
@@ -204,7 +204,7 @@ class GraphQlArgumentInitializerTests {
void primaryConstructorBindingErrorWithNestedBeanList() {
assertThatThrownBy(
() -> initializer.get().initializeArgument(
() -> initializer.get().bind(
environment(
"{\"key\":{\"items\":[" +
"{\"name\":\"first\", \"age\":\"invalid\"}," +