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 14b7d926..223347f4 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 @@ -16,6 +16,7 @@ package org.springframework.graphql.data.method.annotation.support; +import java.util.Map; import java.util.Optional; import graphql.schema.DataFetchingEnvironment; @@ -25,6 +26,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.web.ProjectedPayload; +import org.springframework.graphql.data.ArgumentValue; import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.util.Assert; @@ -90,20 +92,44 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu @Override public boolean supportsParameter(MethodParameter parameter) { - Class type = parameter.nestedIfOptional().getNestedParameterType(); + Class type = getTargetType(parameter); return (type.isInterface() && AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null); } + private static Class getTargetType(MethodParameter parameter) { + Class type = parameter.getParameterType(); + return (type.equals(Optional.class) || type.equals(ArgumentValue.class) ? + parameter.nested().getNestedParameterType() : parameter.getParameterType()); + } + @Override public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception { String name = (parameter.hasParameterAnnotation(Argument.class) ? ArgumentMethodArgumentResolver.getArgumentName(parameter) : null); - 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); + Class targetType = parameter.getParameterType(); + boolean isOptional = (targetType == Optional.class); + boolean isArgumentValue = (targetType == ArgumentValue.class); + + if (isOptional || isArgumentValue) { + targetType = parameter.nested().getNestedParameterType(); + } + + Map arguments = environment.getArguments(); + Object rawValue = (name != null ? arguments.get(name) : arguments); + Object value = (rawValue != null ? createProjection(targetType, rawValue) : null); + + if (isOptional) { + return Optional.ofNullable(value); + } + else if (isArgumentValue) { + return (name != null && arguments.containsKey(name) ? + ArgumentValue.ofNullable(value) : ArgumentValue.omitted()); + } + else { + return value; + } } /** 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 3f243309..1554509f 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,13 +19,13 @@ 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; import org.springframework.core.MethodParameter; import org.springframework.data.web.ProjectedPayload; import org.springframework.graphql.Book; +import org.springframework.graphql.data.ArgumentValue; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.QueryMapping; import org.springframework.stereotype.Controller; @@ -50,11 +50,15 @@ public class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolve @Test void supports() { - MethodParameter param = methodParam(BookController.class, "optionalProjection", Optional.class); - assertThat(this.resolver.supportsParameter(param)).isTrue(); + testSupports("projection", BookProjection.class, true); + testSupports("optionalProjection", Optional.class, true); + testSupports("optionalString", Optional.class, false); + testSupports("argumentValueProjection", ArgumentValue.class, true); + } - param = methodParam(BookController.class, "optionalString", Optional.class); - assertThat(this.resolver.supportsParameter(param)).isFalse(); + void testSupports(String methodName, Class methodParamType, boolean supported) { + MethodParameter param = methodParam(BookController.class, methodName, methodParamType); + assertThat(this.resolver.supportsParameter(param)).isEqualTo(supported); } @Test @@ -81,7 +85,44 @@ public class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolve } @Test - @Disabled // pending decision under gh-550 + void argumentValuePresent() throws Exception { + + Object result = this.resolver.resolveArgument( + methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class), + environment("{ \"where\" : { \"author\" : \"Orwell\" }}")); + + assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class); + BookProjection book = ((ArgumentValue) result).value(); + assertThat(book.getAuthor()).isEqualTo("Orwell"); + } + + @Test + void argumentValueSetToNull() throws Exception { + + Object result = this.resolver.resolveArgument( + methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class), + environment("{ \"where\" : null}")); + + assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class); + ArgumentValue value = ((ArgumentValue) result); + assertThat(value.isPresent()).isFalse(); + assertThat(value.isOmitted()).isFalse(); + } + + @Test + void argumentValueIsOmitted() throws Exception { + + Object result = this.resolver.resolveArgument( + methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class), + environment("{}")); + + assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class); + ArgumentValue value = ((ArgumentValue) result); + assertThat(value.isPresent()).isFalse(); + assertThat(value.isOmitted()).isTrue(); + } + + @Test // gh-550 void nullValue() throws Exception { Object result = this.resolver.resolveArgument( @@ -110,6 +151,11 @@ public class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolve public void optionalString(@Argument Optional projection) { } + @QueryMapping + public List argumentValueProjection(@Argument(name = "where") ArgumentValue projection) { + return null; + } + }