Polishing ProjectedPayloadMethodArgumentResolver

See gh-550
This commit is contained in:
rstoyanchev
2022-12-01 11:38:34 +00:00
parent 0baceda48c
commit d8b3dad505
2 changed files with 70 additions and 34 deletions

View File

@@ -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}.
*
* <p>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
* <p>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.
*
* <p>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.
*
* <p>For example:
* <pre class="code">
* &#064;ProjectedPayload
* interface BookProjection {
* String getName();
* }
*
* &#064;ProjectedPayload
* interface BookProjection {
* String getName();
*
* &#064;Value("#{target.author + ' ' + target.name}")
* String getAuthorAndName();
* }
* </pre>
*
* @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);
}
}

View File

@@ -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<BookProjection>) 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<Book> projection(@Argument(name = "where") BookProjection projection) {
return null;
}
@QueryMapping
public List<Book> optionalProjection(@Argument(name = "where") Optional<BookProjection> projection) {
return null;