diff --git a/spring-graphql-docs/modules/ROOT/pages/controllers.adoc b/spring-graphql-docs/modules/ROOT/pages/controllers.adoc index dc3b5f75..9447b14a 100644 --- a/spring-graphql-docs/modules/ROOT/pages/controllers.adoc +++ b/spring-graphql-docs/modules/ROOT/pages/controllers.adoc @@ -143,11 +143,11 @@ See xref:controllers.adoc#controllers.schema-mapping.argument[`@Argument`]. See xref:controllers.adoc#controllers.schema-mapping.argument[`@Argument`]. -| `ArgumentValue` +| `FieldValue` | 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 xref:controllers.adoc#controllers.schema-mapping.argument-value[`ArgumentValue`]. +See xref:controllers.adoc#controllers.schema-mapping.field-value[`FieldValue`]. | `@Arguments` | For access to all field arguments bound to a higher-level, typed Object. @@ -403,8 +403,8 @@ specified in the annotation, or to the parameter name. For access to the full ar map, please use xref:controllers.adoc#controllers.schema-mapping.arguments[`@Arguments`] instead. -[[controllers.schema-mapping.argument-value]] -=== `ArgumentValue` +[[controllers.schema-mapping.field-value]] +=== `FieldValue` 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 @@ -414,7 +414,7 @@ there is no way to make such a distinction, because you would get `null` or an e `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, +`FieldValue` 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. @@ -427,7 +427,7 @@ For example: public class BookController { @MutationMapping - public void addBook(ArgumentValue bookInput) { + public void addBook(FieldValue bookInput) { if (!bookInput.isOmitted()) { BookInput value = bookInput.value(); // ... @@ -436,7 +436,7 @@ For example: } ---- -`ArgumentValue` is also supported as a field within the object structure of an `@Argument` +`FieldValue` 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. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/FieldValue.java b/spring-graphql/src/main/java/org/springframework/graphql/FieldValue.java new file mode 100644 index 00000000..7fd830a6 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/FieldValue.java @@ -0,0 +1,172 @@ +/* + * Copyright 2020-2025 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; + + +import java.util.Optional; +import java.util.function.Consumer; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Simple container for GraphQL field values that indicates if the field + * value is present, provided but set to {@literal "null"} or omitted altogether. + * + *

In the case of GraphQL mutations, clients send an Input Object with several fields; + * the server must understand the difference between a field being absent + * (the existing value should be left as-is) and a field set to {@literal "null"} + * (the existing value must be set to {@literal "null"}). {@code FieldValue} + * helps to make this distinction. + * + *

Supported in one of the following places: + *

+ * + * @param the type of value contained + * @author Rossen Stoyanchev + * @author Brian Clozel + * @since 1.4.0 + * @see Input Object + * @see Nullable vs Optional + */ +public final class FieldValue { + + private static final FieldValue EMPTY = new FieldValue<>(null, false); + + private static final FieldValue OMITTED = new FieldValue<>(null, true); + + + @Nullable + private final T value; + + private final boolean omitted; + + + private FieldValue(@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 present in the input but the value was {@code null}, + * and {@code false} otherwise. + */ + public boolean isEmpty() { + return !this.omitted && 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 asOptional() { + return Optional.ofNullable(this.value); + } + + /** + * If a value is present, performs the given action with the value, otherwise does nothing. + * @param action the action to be performed, if a value is present + */ + public void ifPresent(Consumer action) { + Assert.notNull(action, "Action is required"); + if (this.value != null) { + action.accept(this.value); + } + } + + @Override + public boolean equals(Object other) { + // This covers EMPTY and OMITTED constant + if (this == other) { + return true; + } + if (!(other instanceof FieldValue 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; + } + + @Override + public String toString() { + if (this.isOmitted()) { + return "FieldValue{omitted}"; + } + return "FieldValue{value=" + this.value + "'}'"; + } + + /** + * Static factory method for an argument value that was provided, even if + * it was set to {@literal "null}. + * @param the type of value + * @param value the value to hold in the instance + */ + @SuppressWarnings("unchecked") + public static FieldValue ofNullable(@Nullable T value) { + return (value != null) ? new FieldValue<>(value, false) : (FieldValue) EMPTY; + } + + /** + * Static factory method for an argument value that was omitted. + * @param the type of value + */ + @SuppressWarnings("unchecked") + public static FieldValue omitted() { + return (FieldValue) OMITTED; + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/ArgumentValue.java b/spring-graphql/src/main/java/org/springframework/graphql/data/ArgumentValue.java index 5ce87f2f..44997eaf 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/ArgumentValue.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/ArgumentValue.java @@ -18,10 +18,8 @@ package org.springframework.graphql.data; import java.util.Optional; -import java.util.function.Consumer; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** @@ -46,7 +44,9 @@ import org.springframework.util.ObjectUtils; * @author Rossen Stoyanchev * @since 1.1.0 * @see Nullable vs Optional + * @deprecated since 1.4.0 in favor of {@link org.springframework.graphql.FieldValue}. */ +@Deprecated(since = "1.4.0", forRemoval = true) public final class ArgumentValue { private static final ArgumentValue EMPTY = new ArgumentValue<>(null, false); @@ -73,15 +73,6 @@ public final class ArgumentValue { return (this.value != null); } - /** - * Return {@code true} if the input value was present in the input but the value was {@code null}, - * and {@code false} otherwise. - * @since 1.4.0 - */ - public boolean isEmpty() { - return !this.omitted && 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 @@ -106,18 +97,6 @@ public final class ArgumentValue { return Optional.ofNullable(this.value); } - /** - * If a value is present, performs the given action with the value, otherwise does nothing. - * @param action the action to be performed, if a value is present - * @since 1.4.0 - */ - public void ifPresent(Consumer action) { - Assert.notNull(action, "Action is required"); - if (this.value != null) { - action.accept(this.value); - } - } - @Override public boolean equals(Object other) { // This covers EMPTY and OMITTED constant diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java index 984874e0..96b9f3c5 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java @@ -40,6 +40,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; +import org.springframework.graphql.FieldValue; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -67,13 +68,15 @@ import org.springframework.validation.FieldError; * *

The binder supports {@link Optional} as a wrapper around any Object or * scalar value in the target Object structure. In addition, it also supports - * {@link ArgumentValue} as a wrapper that indicates whether a given input - * argument was omitted rather than set to the {@literal "null"} literal. + * {@link org.springframework.graphql.FieldValue} as a wrapper that indicates + * whether a given input argument was omitted rather than set to the + * {@literal "null"} literal. * * @author Brian Clozel * @author Rossen Stoyanchev * @since 1.0.0 */ +@SuppressWarnings("removal") public class GraphQlArgumentBinder { @Nullable @@ -115,7 +118,7 @@ public class GraphQlArgumentBinder { * @param name the name of an argument, or {@code null} to use the full map * @param targetType the type of Object to create * @return the created Object, possibly wrapped in {@link Optional} or in - * {@link ArgumentValue}, or {@code null} if there is no value + * {@link org.springframework.graphql.FieldValue}, or {@code null} if there is no value * @throws BindException containing one or more accumulated errors from * matching and/or converting arguments to the target Object */ @@ -175,8 +178,9 @@ public class GraphQlArgumentBinder { boolean isOptional = (targetClass == Optional.class); boolean isArgumentValue = (targetClass == ArgumentValue.class); + boolean isFieldValue = (targetClass == FieldValue.class); - if (isOptional || isArgumentValue) { + if (isOptional || isArgumentValue || isFieldValue) { targetType = targetType.getNested(2); targetClass = targetType.resolve(); } @@ -202,6 +206,9 @@ public class GraphQlArgumentBinder { else if (isArgumentValue) { value = (isOmitted ? ArgumentValue.omitted() : ArgumentValue.ofNullable(value)); } + else if (isFieldValue) { + value = (isOmitted ? FieldValue.omitted() : FieldValue.ofNullable(value)); + } return value; } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java index ed2ec224..58250300 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java @@ -61,6 +61,7 @@ import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.domain.ScrollPosition; +import org.springframework.graphql.FieldValue; import org.springframework.graphql.data.ArgumentValue; import org.springframework.graphql.data.GraphQlArgumentBinder; import org.springframework.graphql.data.method.HandlerMethod; @@ -488,10 +489,12 @@ public class AnnotatedControllerConfigurer } @Override + @SuppressWarnings("removal") public Map getArguments() { Predicate argumentPredicate = (p) -> - (p.getParameterAnnotation(Argument.class) != null || p.getParameterType() == ArgumentValue.class); + (p.getParameterAnnotation(Argument.class) != null || p.getParameterType() == ArgumentValue.class || + p.getParameterType() == FieldValue.class); return Arrays.stream(this.mappingInfo.getHandlerMethod().getMethodParameters()) .filter(argumentPredicate) diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolver.java index f7366853..1ccb1c45 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolver.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolver.java @@ -20,6 +20,7 @@ import graphql.schema.DataFetchingEnvironment; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.graphql.FieldValue; import org.springframework.graphql.data.ArgumentValue; import org.springframework.graphql.data.GraphQlArgumentBinder; import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; @@ -37,12 +38,13 @@ import org.springframework.validation.BindException; * parameter type. * *

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. + * {@link FieldValue} 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. * - *

An {@link ArgumentValue} can also be nested within the object structure + *

An {@link FieldValue} can also be nested within the object structure * of an {@link Argument @Argument}-annotated method parameter. * * @author Rossen Stoyanchev @@ -50,6 +52,7 @@ import org.springframework.validation.BindException; * @since 1.0.0 * @see org.springframework.graphql.data.method.annotation.support.ArgumentsMethodArgumentResolver */ +@SuppressWarnings("removal") public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver { private final GraphQlArgumentBinder argumentBinder; @@ -72,7 +75,8 @@ public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentReso @Override public boolean supportsParameter(MethodParameter parameter) { return (parameter.getParameterAnnotation(Argument.class) != null || - parameter.getParameterType() == ArgumentValue.class); + parameter.getParameterType() == ArgumentValue.class || + parameter.getParameterType() == FieldValue.class); } @Override @@ -103,9 +107,10 @@ public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentReso return argument.name(); } } - else if (parameter.getParameterType() != ArgumentValue.class) { + else if (parameter.getParameterType() != ArgumentValue.class && + parameter.getParameterType() != FieldValue.class) { throw new IllegalStateException( - "Expected either @Argument or a method parameter of type ArgumentValue"); + "Expected either @Argument or a method parameter of type FieldValue"); } String parameterName = parameter.getParameterName(); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentValueValueExtractor.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentValueValueExtractor.java index bf8eb572..0104a555 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentValueValueExtractor.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentValueValueExtractor.java @@ -30,6 +30,7 @@ import org.springframework.graphql.data.ArgumentValue; * @since 1.2.2 */ @UnwrapByDefault +@SuppressWarnings("removal") public final class ArgumentValueValueExtractor implements ValueExtractor> { @Override diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/FieldValueValueExtractor.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/FieldValueValueExtractor.java new file mode 100644 index 00000000..9819dd9f --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/FieldValueValueExtractor.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2025 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.method.annotation.support; + +import jakarta.validation.valueextraction.ExtractedValue; +import jakarta.validation.valueextraction.UnwrapByDefault; +import jakarta.validation.valueextraction.ValueExtractor; + +import org.springframework.graphql.FieldValue; + +/** + * {@link ValueExtractor} that enables {@code @Valid} with {@link FieldValue}, + * and helps to extract the value from it. + * + * @author Rossen Stoyanchev + * @since 1.4.0 + */ +@UnwrapByDefault +public final class FieldValueValueExtractor implements ValueExtractor> { + + @Override + public void extractValues(FieldValue fieldValue, ValueReceiver receiver) { + if (!fieldValue.isOmitted()) { + receiver.value(null, fieldValue.value()); + } + } + +} 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 628f7a30..7658cd78 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 @@ -27,6 +27,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.FieldValue; import org.springframework.graphql.data.ArgumentValue; import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; import org.springframework.graphql.data.method.annotation.Argument; @@ -97,13 +98,16 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu return (type.isInterface() && AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null); } + @SuppressWarnings("removal") private static Class getTargetType(MethodParameter parameter) { Class type = parameter.getParameterType(); - return (type.equals(Optional.class) || type.equals(ArgumentValue.class)) ? + return (type.equals(Optional.class) || type.equals(ArgumentValue.class) || + type.equals(FieldValue.class)) ? parameter.nested().getNestedParameterType() : parameter.getParameterType(); } @Override + @SuppressWarnings("removal") public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception { String name = (parameter.hasParameterAnnotation(Argument.class) ? @@ -112,8 +116,9 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu Class targetType = parameter.getParameterType(); boolean isOptional = (targetType == Optional.class); boolean isArgumentValue = (targetType == ArgumentValue.class); + boolean isFieldValue = (targetType == FieldValue.class); - if (isOptional || isArgumentValue) { + if (isOptional || isArgumentValue || isFieldValue) { targetType = parameter.nested().getNestedParameterType(); } @@ -128,6 +133,10 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu return (name != null && arguments.containsKey(name)) ? ArgumentValue.ofNullable(value) : ArgumentValue.omitted(); } + else if (isFieldValue) { + return (name != null && arguments.containsKey(name)) ? + FieldValue.ofNullable(value) : FieldValue.omitted(); + } else { return value; } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessor.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessor.java index da47ab22..514baf56 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessor.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessor.java @@ -43,6 +43,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.TargetAware; +import org.springframework.graphql.FieldValue; import org.springframework.graphql.data.ArgumentValue; import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite; @@ -245,9 +246,11 @@ class SchemaMappingBeanFactoryInitializationAotProcessor implements BeanFactoryI } @Override + @SuppressWarnings("removal") public void apply(RuntimeHints runtimeHints) { Type parameterType = this.methodParameter.getGenericParameterType(); - if (ArgumentValue.class.isAssignableFrom(this.methodParameter.getParameterType())) { + if (ArgumentValue.class.isAssignableFrom(this.methodParameter.getParameterType()) || + FieldValue.class.isAssignableFrom(this.methodParameter.getParameterType())) { parameterType = this.methodParameter.nested().getNestedGenericParameterType(); } bindingRegistrar.registerReflectionHints(runtimeHints.reflection(), parameterType); diff --git a/spring-graphql/src/main/resources/META-INF/services/jakarta.validation.valueextraction.ValueExtractor b/spring-graphql/src/main/resources/META-INF/services/jakarta.validation.valueextraction.ValueExtractor index 410cf01b..9809d7cd 100644 --- a/spring-graphql/src/main/resources/META-INF/services/jakarta.validation.valueextraction.ValueExtractor +++ b/spring-graphql/src/main/resources/META-INF/services/jakarta.validation.valueextraction.ValueExtractor @@ -1 +1,2 @@ +org.springframework.graphql.data.method.annotation.support.FieldValueValueExtractor org.springframework.graphql.data.method.annotation.support.ArgumentValueValueExtractor \ No newline at end of file diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/ArgumentValueTests.java b/spring-graphql/src/test/java/org/springframework/graphql/FieldValueTests.java similarity index 71% rename from spring-graphql/src/test/java/org/springframework/graphql/data/ArgumentValueTests.java rename to spring-graphql/src/test/java/org/springframework/graphql/FieldValueTests.java index fdfa4f0a..918ffc22 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/ArgumentValueTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/FieldValueTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.graphql.data; +package org.springframework.graphql; import java.util.concurrent.atomic.AtomicBoolean; @@ -23,14 +23,14 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link ArgumentValue}. + * Tests for {@link FieldValue}. * @author Brian Clozel */ -class ArgumentValueTests { +class FieldValueTests { @Test void existingValueShouldBePresent() { - ArgumentValue message = ArgumentValue.ofNullable("hello"); + FieldValue message = FieldValue.ofNullable("hello"); assertThat(message.isOmitted()).isFalse(); assertThat(message.isPresent()).isTrue(); assertThat(message.isEmpty()).isFalse(); @@ -38,7 +38,7 @@ class ArgumentValueTests { @Test void nullValueShouldBePresent() { - ArgumentValue message = ArgumentValue.ofNullable(null); + FieldValue message = FieldValue.ofNullable(null); assertThat(message.isOmitted()).isFalse(); assertThat(message.isPresent()).isFalse(); assertThat(message.isEmpty()).isTrue(); @@ -46,7 +46,7 @@ class ArgumentValueTests { @Test void noValueShouldBeOmitted() { - ArgumentValue message = ArgumentValue.omitted(); + FieldValue message = FieldValue.omitted(); assertThat(message.isOmitted()).isTrue(); assertThat(message.isPresent()).isFalse(); assertThat(message.isEmpty()).isFalse(); @@ -54,29 +54,29 @@ class ArgumentValueTests { @Test void asOptionalShouldMapOmitted() { - assertThat(ArgumentValue.omitted().asOptional()).isEmpty(); - assertThat(ArgumentValue.ofNullable(null).asOptional()).isEmpty(); - assertThat(ArgumentValue.ofNullable("hello").asOptional()).isPresent(); + assertThat(FieldValue.omitted().asOptional()).isEmpty(); + assertThat(FieldValue.ofNullable(null).asOptional()).isEmpty(); + assertThat(FieldValue.ofNullable("hello").asOptional()).isPresent(); } @Test void ifPresentShouldExecuteWhenValue() { AtomicBoolean called = new AtomicBoolean(); - ArgumentValue.ofNullable("hello").ifPresent(value -> called.set(true)); + FieldValue.ofNullable("hello").ifPresent(value -> called.set(true)); assertThat(called.get()).isTrue(); } @Test void ifPresentShouldSkipWhenNull() { AtomicBoolean called = new AtomicBoolean(); - ArgumentValue.ofNullable(null).ifPresent(value -> called.set(true)); + FieldValue.ofNullable(null).ifPresent(value -> called.set(true)); assertThat(called.get()).isFalse(); } @Test void ifPresentShouldSkipWhenOmitted() { AtomicBoolean called = new AtomicBoolean(); - ArgumentValue.omitted().ifPresent(value -> called.set(true)); + FieldValue.omitted().ifPresent(value -> called.set(true)); assertThat(called.get()).isFalse(); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java index 680802e6..b9356b7b 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. @@ -38,6 +38,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.graphql.Book; +import org.springframework.graphql.FieldValue; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.lang.Nullable; import org.springframework.validation.BindException; @@ -53,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; * @author Brian Clozel * @author Rossen Stoyanchev */ +@SuppressWarnings("removal") class GraphQlArgumentBinderTests { private final ObjectMapper mapper = new ObjectMapper(); @@ -248,6 +250,30 @@ class GraphQlArgumentBinderTests { assertThat(itemBean.getName().isPresent()).isFalse(); } + @Test + void primaryConstructorWithOptionalFieldBeanArgument() throws Exception { + + ResolvableType targetType = + ResolvableType.forClass(PrimaryConstructorOptionalFieldItemBean.class); + + Object result = bind( + "{\"item\":{\"name\":\"Item name\",\"age\":\"30\"},\"name\":\"Hello\"}", targetType); + + assertThat(result).isInstanceOf(PrimaryConstructorOptionalFieldItemBean.class).isNotNull(); + PrimaryConstructorOptionalFieldItemBean itemBean = (PrimaryConstructorOptionalFieldItemBean) result; + + assertThat(itemBean.getItem().value().getName()).isEqualTo("Item name"); + assertThat(itemBean.getItem().value().getAge()).isEqualTo(30); + assertThat(itemBean.getName().value()).isEqualTo("Hello"); + + result = bind("{\"key\":{}}", targetType); + itemBean = (PrimaryConstructorOptionalFieldItemBean) result; + + assertThat(itemBean).isNotNull(); + assertThat(itemBean.getItem().isOmitted()).isTrue(); + assertThat(itemBean.getName().isPresent()).isFalse(); + } + @Test void primaryConstructorWithNestedBeanList() throws Exception { @@ -570,6 +596,26 @@ class GraphQlArgumentBinderTests { } } + static class PrimaryConstructorOptionalFieldItemBean { + + private final FieldValue name; + + private final FieldValue item; + + public PrimaryConstructorOptionalFieldItemBean(FieldValue name, FieldValue item) { + this.name = name; + this.item = item; + } + + public FieldValue getName() { + return this.name; + } + + public FieldValue getItem() { + return item; + } + } + @SuppressWarnings("unused") static class NoPrimaryConstructorBean { diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolverTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolverTests.java index 1293fcab..8c8f665a 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolverTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2025 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. @@ -25,6 +25,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.FieldValue; import org.springframework.graphql.data.ArgumentValue; import org.springframework.graphql.data.GraphQlArgumentBinder; import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; @@ -53,6 +54,9 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport { param = methodParam(BookController.class, "addBook", ArgumentValue.class); assertThat(this.resolver.supportsParameter(param)).isTrue(); + param = methodParam(BookController.class, "addBook", FieldValue.class); + assertThat(this.resolver.supportsParameter(param)).isTrue(); + param = methodParam(BookController.class, "rawArgumentValue", Map.class); assertThat(this.resolver.supportsParameter(param)).isTrue(); @@ -81,7 +85,7 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport { } @Test - void shouldResolveJavaBeanArgumentWithWrapper() throws Exception { + void shouldResolveJavaBeanArgumentWithArgumentWrapper() throws Exception { Object result = this.resolver.resolveArgument( methodParam(BookController.class, "addBook", ArgumentValue.class), environment("{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42} }")); @@ -94,6 +98,20 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport { .hasFieldOrPropertyWithValue("authorId", 42L); } + @Test + void shouldResolveJavaBeanArgumentWithFieldWrapper() throws Exception { + Object result = this.resolver.resolveArgument( + methodParam(BookController.class, "addBook", FieldValue.class), + environment("{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42} }")); + + assertThat(result) + .isNotNull() + .isInstanceOf(FieldValue.class) + .extracting(value -> ((FieldValue) value).value()) + .hasFieldOrPropertyWithValue("name", "test name") + .hasFieldOrPropertyWithValue("authorId", 42L); + } + @Test void shouldResolveListOfJavaBeansArgument() throws Exception { Object result = this.resolver.resolveArgument( @@ -152,6 +170,11 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport { return null; } + @MutationMapping + public Book addBook(FieldValue bookInput) { + return null; + } + @MutationMapping public List addBooks(@Argument List books) { return null; diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentsMethodArgumentResolverTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentsMethodArgumentResolverTests.java index 66ef2ed2..9a748bf0 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentsMethodArgumentResolverTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentsMethodArgumentResolverTests.java @@ -25,7 +25,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.FieldValue; import org.springframework.graphql.data.GraphQlArgumentBinder; import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; import org.springframework.graphql.data.method.annotation.Arguments; @@ -109,15 +109,15 @@ class ArgumentsMethodArgumentResolverTests extends ArgumentResolverTestSupport { @SuppressWarnings({"NotNullFieldNotInitialized", "unused"}) static class BookInput { - ArgumentValue name; + FieldValue name; Long authorId; - public ArgumentValue getName() { + public FieldValue getName() { return this.name; } - public void setName(ArgumentValue name) { + public void setName(FieldValue name) { this.name = name; } 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 70f08eb0..85da6d55 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 @@ -26,7 +26,7 @@ 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.FieldValue; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.QueryMapping; import org.springframework.stereotype.Controller; @@ -54,7 +54,7 @@ class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolverTestSu testSupports("projection", BookProjection.class, true); testSupports("optionalProjection", Optional.class, true); testSupports("optionalString", Optional.class, false); - testSupports("argumentValueProjection", ArgumentValue.class, true); + testSupports("fieldValueProjection", FieldValue.class, true); } void testSupports(String methodName, Class methodParamType, boolean supported) { @@ -86,39 +86,39 @@ class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolverTestSu } @Test - void argumentValuePresent() throws Exception { + void fieldValuePresent() throws Exception { Object result = this.resolver.resolveArgument( - methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class), + methodParam(BookController.class, "fieldValueProjection", FieldValue.class), environment("{ \"where\" : { \"author\" : \"Orwell\" }}")); - assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class); - BookProjection book = ((ArgumentValue) result).value(); + assertThat(result).isNotNull().isInstanceOf(FieldValue.class); + BookProjection book = ((FieldValue) result).value(); assertThat(book.getAuthor()).isEqualTo("Orwell"); } @Test - void argumentValueSetToNull() throws Exception { + void fieldValueSetToNull() throws Exception { Object result = this.resolver.resolveArgument( - methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class), + methodParam(BookController.class, "fieldValueProjection", FieldValue.class), environment("{ \"where\" : null}")); - assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class); - ArgumentValue value = ((ArgumentValue) result); + assertThat(result).isNotNull().isInstanceOf(FieldValue.class); + FieldValue value = ((FieldValue) result); assertThat(value.isPresent()).isFalse(); assertThat(value.isOmitted()).isFalse(); } @Test - void argumentValueIsOmitted() throws Exception { + void fieldValueIsOmitted() throws Exception { Object result = this.resolver.resolveArgument( - methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class), + methodParam(BookController.class, "fieldValueProjection", FieldValue.class), environment("{}")); - assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class); - ArgumentValue value = ((ArgumentValue) result); + assertThat(result).isNotNull().isInstanceOf(FieldValue.class); + FieldValue value = ((FieldValue) result); assertThat(value.isPresent()).isFalse(); assertThat(value.isOmitted()).isTrue(); } @@ -153,7 +153,7 @@ class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolverTestSu } @QueryMapping - public List argumentValueProjection(@Argument(name = "where") ArgumentValue projection) { + public List fieldValueProjection(@Argument(name = "where") FieldValue projection) { return null; } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessorTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessorTests.java index 295aa563..fede9c68 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessorTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingBeanFactoryInitializationAotProcessorTests.java @@ -58,7 +58,7 @@ import org.springframework.data.projection.TargetAware; import org.springframework.data.web.ProjectedPayload; import org.springframework.graphql.Author; import org.springframework.graphql.Book; -import org.springframework.graphql.data.ArgumentValue; +import org.springframework.graphql.FieldValue; import org.springframework.graphql.data.federation.EntityMapping; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.BatchMapping; @@ -120,12 +120,12 @@ class SchemaMappingBeanFactoryInitializationAotProcessorTests { } @Test - void registerBindingReflectionOnArgumentValue() { - processBeanClasses(ArgumentValueController.class); - assertThatIntrospectionOnMethodsHintRegisteredForType(ArgumentValueController.class); - assertThatInvocationHintRegisteredForMethods(ArgumentValueController.class, "addBook"); + void registerBindingReflectionOnFieldValue() { + processBeanClasses(FieldValueController.class); + assertThatIntrospectionOnMethodsHintRegisteredForType(FieldValueController.class); + assertThatInvocationHintRegisteredForMethods(FieldValueController.class, "addBook"); assertThatHintsForJavaBeanBindingRegisteredForTypes(Book.class, BookInput.class); - assertThatHintsAreNotRegisteredForTypes(ArgumentValue.class); + assertThatHintsAreNotRegisteredForTypes(FieldValue.class); } @Test @@ -173,9 +173,9 @@ class SchemaMappingBeanFactoryInitializationAotProcessorTests { } @Controller - static class ArgumentValueController { + static class FieldValueController { @MutationMapping - public Book addBook(ArgumentValue bookInput) { + public Book addBook(FieldValue bookInput) { return null; } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ValidationHelperTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ValidationHelperTests.java index 4b0abbf4..d7a31fa4 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ValidationHelperTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ValidationHelperTests.java @@ -36,7 +36,7 @@ import org.assertj.core.api.ThrowableAssert; import org.junit.jupiter.api.Test; import org.springframework.beans.BeanUtils; -import org.springframework.graphql.data.ArgumentValue; +import org.springframework.graphql.FieldValue; import org.springframework.graphql.data.method.HandlerMethod; import org.springframework.validation.annotation.Validated; @@ -71,20 +71,20 @@ class ValidationHelperTests { BiConsumer validator2 = validateFunction(MyBean.class, "myValidatedParameterMethod"); assertViolation(() -> validator2.accept(bean, new Object[] {new ConstrainedInput(100)}), "integerValue"); - BiConsumer validator3 = validateFunction(MyBean.class, "myValidArgumentValue"); - assertViolation(() -> validator3.accept(bean, new Object[] {ArgumentValue.ofNullable("")}), "myValidArgumentValue.arg0"); + BiConsumer validator3 = validateFunction(MyBean.class, "myValidFieldValue"); + assertViolation(() -> validator3.accept(bean, new Object[] {FieldValue.ofNullable("")}), "myValidFieldValue.arg0"); // Validate that an explicit null value is validated. - assertViolation(() -> validator3.accept(bean, new Object[] {ArgumentValue.ofNullable(null)}), "myValidArgumentValue.arg0"); + assertViolation(() -> validator3.accept(bean, new Object[] {FieldValue.ofNullable(null)}), "myValidFieldValue.arg0"); } @Test - void shouldNotRaiseValidationErrorForOmittedArgumentValue() { + void shouldNotRaiseValidationErrorForOmittedFieldValue() { MyBean bean = new MyBean(); // Validate that an omitted value is allowed. - BiConsumer validator3 = validateFunction(MyBean.class, "myValidArgumentValue"); - validator3.accept(bean, new Object[] {ArgumentValue.omitted()}); + BiConsumer validator3 = validateFunction(MyBean.class, "myValidFieldValue"); + validator3.accept(bean, new Object[] {FieldValue.omitted()}); } @Test @@ -173,7 +173,7 @@ class ValidationHelperTests { return null; } - public Object myValidArgumentValue(@Valid ArgumentValue<@NotBlank String> arg0) { + public Object myValidFieldValue(@Valid FieldValue<@NotBlank String> arg0) { return null; } }