diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ProjectedPayloadMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ProjectedPayloadMethodArgumentResolver.java index ab6be6ee..14b7d926 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ProjectedPayloadMethodArgumentResolver.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ProjectedPayloadMethodArgumentResolver.java @@ -30,32 +30,34 @@ import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.util.Assert; /** - * Resolver to obtain an {@link ProjectedPayload @ProjectedPayload}, - * either based on the complete {@link DataFetchingEnvironment#getArguments()} - * map, or based on a specific argument within the map when the method - * parameter is annotated with {@code @Argument}. + * Resolver for a method parameter that is an interface annotated with + * {@link ProjectedPayload @ProjectedPayload}. * - *

Projected payloads consist of the projection interface and accessor - * methods. Projections can be closed or open projections. Closed projections - * use interface getter methods to access underlying properties directly. - * Open projection methods make use of the {@code @Value} annotation to + *

By default, the projection is prepared by using the complete + * {@link DataFetchingEnvironment#getArguments() arguments map} as its source. + * Add {@link Argument @Argument} with a name, if you to prepare it by using a + * specific argument value instead as its source. + * + *

An {@code @ProjectedPayload} interface has accessor methods. In a closed + * projection, getter methods access underlying properties directly. In an open + * projection, getter methods make use of the {@code @Value} annotation to * evaluate SpEL expressions against the underlying {@code target} object. * *

For example: *

  * @ProjectedPayload
  * interface BookProjection {
- *   String getName();
- * }
  *
- * @ProjectedPayload
- * interface BookProjection {
+ *   String getName();
+ *
  *   @Value("#{target.author + ' '  + target.name}")
  *   String getAuthorAndName();
  * }
  * 
* * @author Mark Paluch + * @author Rossen Stoyanchev + * * @since 1.0.0 */ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgumentResolver { @@ -77,11 +79,19 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu } + /** + * Return underlying projection factory used by the resolver. + * @since 1.1.1 + */ + protected SpelAwareProxyProjectionFactory getProjectionFactory() { + return this.projectionFactory; + } + + @Override public boolean supportsParameter(MethodParameter parameter) { Class type = parameter.nestedIfOptional().getNestedParameterType(); - return (type.isInterface() && - AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null); + return (type.isInterface() && AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null); } @Override @@ -90,26 +100,22 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu String name = (parameter.hasParameterAnnotation(Argument.class) ? ArgumentMethodArgumentResolver.getArgumentName(parameter) : null); - Class projectionType = parameter.getParameterType(); - - boolean isOptional = parameter.isOptional(); - if (isOptional) { - projectionType = parameter.nestedIfOptional().getNestedParameterType(); - } - - Object projectionSource = (name != null ? - environment.getArgument(name) : environment.getArguments()); - - Object value = null; - if (!isOptional || projectionSource != null) { - value = project(projectionType, projectionSource); - } - - return (isOptional ? Optional.ofNullable(value) : value); + Class targetType = parameter.nestedIfOptional().getNestedParameterType(); + Object rawValue = (name != null ? environment.getArgument(name) : environment.getArguments()); + Object value = (!parameter.isOptional() || rawValue != null ? createProjection(targetType, rawValue) : null); + return (parameter.isOptional() ? Optional.ofNullable(value) : value); } - protected Object project(Class projectionType, Object projectionSource){ - return this.projectionFactory.createProjection(projectionType, projectionSource); + /** + * Protected method to create the projection. The default implementation + * delegates to the underlying {@link #getProjectionFactory() projectionFactory}. + * @param targetType the type to create + * @param rawValue a specific argument (if named via {@link Argument} + * or the map of arguments + * @return the created project instance + */ + protected Object createProjection(Class targetType, Object rawValue){ + return this.projectionFactory.createProjection(targetType, rawValue); } } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ProjectedPayloadMethodArgumentResolverTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ProjectedPayloadMethodArgumentResolverTests.java index 22ac8108..3f243309 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ProjectedPayloadMethodArgumentResolverTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ProjectedPayloadMethodArgumentResolverTests.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.context.support.StaticApplicationContext; @@ -32,7 +33,8 @@ import org.springframework.stereotype.Controller; import static org.assertj.core.api.Assertions.assertThat; /** - * + * Unit tests for {@link ProjectedPayloadMethodArgumentResolver}. + * @author Rossen Stoyanchev */ public class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolverTestSupport { @@ -56,7 +58,19 @@ public class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolve } @Test - void optionalWrapper() throws Exception { + void optionalPresent() throws Exception { + + Object result = this.resolver.resolveArgument( + methodParam(BookController.class, "optionalProjection", Optional.class), + environment("{ \"where\" : { \"author\" : \"Orwell\" }}")); + + assertThat(result).isNotNull().isInstanceOf(Optional.class); + BookProjection book = ((Optional) result).get(); + assertThat(book.getAuthor()).isEqualTo("Orwell"); + } + + @Test + void optionalNotPresent() throws Exception { Object result = this.resolver.resolveArgument( methodParam(BookController.class, "optionalProjection", Optional.class), @@ -66,11 +80,27 @@ public class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolve assertThat((Optional) result).isNotPresent(); } + @Test + @Disabled // pending decision under gh-550 + void nullValue() throws Exception { + + Object result = this.resolver.resolveArgument( + methodParam(BookController.class, "projection", BookProjection.class), + environment("{}")); + + assertThat(result).isNull(); + } + @SuppressWarnings({"ConstantConditions", "unused"}) @Controller static class BookController { + @QueryMapping + public List projection(@Argument(name = "where") BookProjection projection) { + return null; + } + @QueryMapping public List optionalProjection(@Argument(name = "where") Optional projection) { return null;