Skip to content

Commit 9b9761f

Browse files
committed
Rename ArgumentValue to FieldValue
Prior to this commit, `ArgumentValue<T>` would mainly focus on the server-side support with the binding of arguments on Controller methods. With the introduction of this feature on the client in gh-1174, this commit reconsiders both the `ArgumentValue<T>` name and its package location to reflect the broader support. This commit deprecates `ArgumentValue<T>` in favor of `FieldValue<T>` with similar support. Closes gh-1187
1 parent b1e2e9a commit 9b9761f

18 files changed

+387
-96
lines changed

spring-graphql-docs/modules/ROOT/pages/controllers.adoc

+7-7
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,11 @@ See xref:controllers.adoc#controllers.schema-mapping.argument[`@Argument`].
143143

144144
See xref:controllers.adoc#controllers.schema-mapping.argument[`@Argument`].
145145

146-
| `ArgumentValue`
146+
| `FieldValue`
147147
| For access to a named field argument bound to a higher-level, typed Object along
148148
with a flag to indicate if the input argument was omitted vs set to `null`.
149149

150-
See xref:controllers.adoc#controllers.schema-mapping.argument-value[`ArgumentValue`].
150+
See xref:controllers.adoc#controllers.schema-mapping.field-value[`FieldValue`].
151151

152152
| `@Arguments`
153153
| 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
403403
map, please use xref:controllers.adoc#controllers.schema-mapping.arguments[`@Arguments`] instead.
404404

405405

406-
[[controllers.schema-mapping.argument-value]]
407-
=== `ArgumentValue`
406+
[[controllers.schema-mapping.field-value]]
407+
=== `FieldValue`
408408

409409
By default, input arguments in GraphQL are nullable and optional, which means an argument
410410
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
414414
`Optional` in both cases.
415415

416416
If you want to know not whether a value was not provided at all, you can declare an
417-
`ArgumentValue` method parameter, which is a simple container for the resulting value,
417+
`FieldValue` method parameter, which is a simple container for the resulting value,
418418
along with a flag to indicate whether the input argument was omitted altogether. You
419419
can use this instead of `@Argument`, in which case the argument name is determined from
420420
the method parameter name, or together with `@Argument` to specify the argument name.
@@ -427,7 +427,7 @@ For example:
427427
public class BookController {
428428
429429
@MutationMapping
430-
public void addBook(ArgumentValue<BookInput> bookInput) {
430+
public void addBook(FieldValue<BookInput> bookInput) {
431431
if (!bookInput.isOmitted()) {
432432
BookInput value = bookInput.value();
433433
// ...
@@ -436,7 +436,7 @@ For example:
436436
}
437437
----
438438

439-
`ArgumentValue` is also supported as a field within the object structure of an `@Argument`
439+
`FieldValue` is also supported as a field within the object structure of an `@Argument`
440440
method parameter, either initialized via a constructor argument or via a setter, including
441441
as a field of an object nested at any level below the top level object.
442442

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql;
18+
19+
20+
import java.util.Optional;
21+
import java.util.function.Consumer;
22+
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.util.Assert;
25+
import org.springframework.util.ObjectUtils;
26+
27+
/**
28+
* Simple container for GraphQL field values that indicates if the field
29+
* value is present, provided but set to {@literal "null"} or omitted altogether.
30+
*
31+
* <p>In the case of GraphQL mutations, clients send an Input Object with several fields;
32+
* the server must understand the difference between a field being absent
33+
* (the existing value should be left as-is) and a field set to {@literal "null"}
34+
* (the existing value must be set to {@literal "null"}). {@code FieldValue<T>}
35+
* helps to make this distinction.
36+
*
37+
* <p>Supported in one of the following places:
38+
* <ul>
39+
* <li>On a controller method parameter, either instead of
40+
* {@link org.springframework.graphql.data.method.annotation.Argument @Argument}
41+
* in which case the argument name is determined from the method parameter name,
42+
* or together with {@code @Argument} to specify the argument name.
43+
* <li>As a field within the object structure of an {@code @Argument} method
44+
* parameter, either initialized via a constructor argument or a setter,
45+
* including as a field of an object nested at any level below the top level
46+
* object.
47+
* </ul>
48+
*
49+
* @param <T> the type of value contained
50+
* @author Rossen Stoyanchev
51+
* @author Brian Clozel
52+
* @since 1.4.0
53+
* @see <a href="http://spec.graphql.org/October2021/#sec-Input-Objects">Input Object</a>
54+
* @see <a href="http://spec.graphql.org/October2021/#sec-Non-Null.Nullable-vs-Optional">Nullable vs Optional</a>
55+
*/
56+
public final class FieldValue<T> {
57+
58+
private static final FieldValue<?> EMPTY = new FieldValue<>(null, false);
59+
60+
private static final FieldValue<?> OMITTED = new FieldValue<>(null, true);
61+
62+
63+
@Nullable
64+
private final T value;
65+
66+
private final boolean omitted;
67+
68+
69+
private FieldValue(@Nullable T value, boolean omitted) {
70+
this.value = value;
71+
this.omitted = omitted;
72+
}
73+
74+
75+
/**
76+
* Return {@code true} if a non-null value is present, and {@code false} otherwise.
77+
*/
78+
public boolean isPresent() {
79+
return (this.value != null);
80+
}
81+
82+
/**
83+
* Return {@code true} if the input value was present in the input but the value was {@code null},
84+
* and {@code false} otherwise.
85+
*/
86+
public boolean isEmpty() {
87+
return !this.omitted && this.value == null;
88+
}
89+
90+
/**
91+
* Return {@code true} if the input value was omitted altogether from the
92+
* input, and {@code false} if it was provided, but possibly set to the
93+
* {@literal "null"} literal.
94+
*/
95+
public boolean isOmitted() {
96+
return this.omitted;
97+
}
98+
99+
/**
100+
* Return the contained value, or {@code null}.
101+
*/
102+
@Nullable
103+
public T value() {
104+
return this.value;
105+
}
106+
107+
/**
108+
* Return the contained value as a nullable {@link Optional}.
109+
*/
110+
public Optional<T> asOptional() {
111+
return Optional.ofNullable(this.value);
112+
}
113+
114+
/**
115+
* If a value is present, performs the given action with the value, otherwise does nothing.
116+
* @param action the action to be performed, if a value is present
117+
*/
118+
public void ifPresent(Consumer<? super T> action) {
119+
Assert.notNull(action, "Action is required");
120+
if (this.value != null) {
121+
action.accept(this.value);
122+
}
123+
}
124+
125+
@Override
126+
public boolean equals(Object other) {
127+
// This covers EMPTY and OMITTED constant
128+
if (this == other) {
129+
return true;
130+
}
131+
if (!(other instanceof FieldValue<?> otherValue)) {
132+
return false;
133+
}
134+
return ObjectUtils.nullSafeEquals(this.value, otherValue.value);
135+
}
136+
137+
@Override
138+
public int hashCode() {
139+
int result = ObjectUtils.nullSafeHashCode(this.value);
140+
result = 31 * result + Boolean.hashCode(this.omitted);
141+
return result;
142+
}
143+
144+
@Override
145+
public String toString() {
146+
if (this.isOmitted()) {
147+
return "FieldValue{omitted}";
148+
}
149+
return "FieldValue{value=" + this.value + "'}'";
150+
}
151+
152+
/**
153+
* Static factory method for an argument value that was provided, even if
154+
* it was set to {@literal "null}.
155+
* @param <T> the type of value
156+
* @param value the value to hold in the instance
157+
*/
158+
@SuppressWarnings("unchecked")
159+
public static <T> FieldValue<T> ofNullable(@Nullable T value) {
160+
return (value != null) ? new FieldValue<>(value, false) : (FieldValue<T>) EMPTY;
161+
}
162+
163+
/**
164+
* Static factory method for an argument value that was omitted.
165+
* @param <T> the type of value
166+
*/
167+
@SuppressWarnings("unchecked")
168+
public static <T> FieldValue<T> omitted() {
169+
return (FieldValue<T>) OMITTED;
170+
}
171+
172+
}

spring-graphql/src/main/java/org/springframework/graphql/data/ArgumentValue.java

+2-23
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@
1818

1919

2020
import java.util.Optional;
21-
import java.util.function.Consumer;
2221

2322
import org.springframework.lang.Nullable;
24-
import org.springframework.util.Assert;
2523
import org.springframework.util.ObjectUtils;
2624

2725
/**
@@ -46,7 +44,9 @@
4644
* @author Rossen Stoyanchev
4745
* @since 1.1.0
4846
* @see <a href="http://spec.graphql.org/October2021/#sec-Non-Null.Nullable-vs-Optional">Nullable vs Optional</a>
47+
* @deprecated since 1.4.0 in favor of {@link org.springframework.graphql.FieldValue}.
4948
*/
49+
@Deprecated(since = "1.4.0", forRemoval = true)
5050
public final class ArgumentValue<T> {
5151

5252
private static final ArgumentValue<?> EMPTY = new ArgumentValue<>(null, false);
@@ -73,15 +73,6 @@ public boolean isPresent() {
7373
return (this.value != null);
7474
}
7575

76-
/**
77-
* Return {@code true} if the input value was present in the input but the value was {@code null},
78-
* and {@code false} otherwise.
79-
* @since 1.4.0
80-
*/
81-
public boolean isEmpty() {
82-
return !this.omitted && this.value == null;
83-
}
84-
8576
/**
8677
* Return {@code true} if the input value was omitted altogether from the
8778
* input, and {@code false} if it was provided, but possibly set to the
@@ -106,18 +97,6 @@ public Optional<T> asOptional() {
10697
return Optional.ofNullable(this.value);
10798
}
10899

109-
/**
110-
* If a value is present, performs the given action with the value, otherwise does nothing.
111-
* @param action the action to be performed, if a value is present
112-
* @since 1.4.0
113-
*/
114-
public void ifPresent(Consumer<? super T> action) {
115-
Assert.notNull(action, "Action is required");
116-
if (this.value != null) {
117-
action.accept(this.value);
118-
}
119-
}
120-
121100
@Override
122101
public boolean equals(Object other) {
123102
// This covers EMPTY and OMITTED constant

spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java

+11-4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.core.convert.ConversionService;
4141
import org.springframework.core.convert.TypeDescriptor;
4242
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
43+
import org.springframework.graphql.FieldValue;
4344
import org.springframework.lang.Nullable;
4445
import org.springframework.util.ClassUtils;
4546
import org.springframework.util.ReflectionUtils;
@@ -67,13 +68,15 @@
6768
*
6869
* <p>The binder supports {@link Optional} as a wrapper around any Object or
6970
* scalar value in the target Object structure. In addition, it also supports
70-
* {@link ArgumentValue} as a wrapper that indicates whether a given input
71-
* argument was omitted rather than set to the {@literal "null"} literal.
71+
* {@link org.springframework.graphql.FieldValue} as a wrapper that indicates
72+
* whether a given input argument was omitted rather than set to the
73+
* {@literal "null"} literal.
7274
*
7375
* @author Brian Clozel
7476
* @author Rossen Stoyanchev
7577
* @since 1.0.0
7678
*/
79+
@SuppressWarnings("removal")
7780
public class GraphQlArgumentBinder {
7881

7982
@Nullable
@@ -115,7 +118,7 @@ private static SimpleTypeConverter initTypeConverter(@Nullable ConversionService
115118
* @param name the name of an argument, or {@code null} to use the full map
116119
* @param targetType the type of Object to create
117120
* @return the created Object, possibly wrapped in {@link Optional} or in
118-
* {@link ArgumentValue}, or {@code null} if there is no value
121+
* {@link org.springframework.graphql.FieldValue}, or {@code null} if there is no value
119122
* @throws BindException containing one or more accumulated errors from
120123
* matching and/or converting arguments to the target Object
121124
*/
@@ -175,8 +178,9 @@ private Object bindRawValue(
175178

176179
boolean isOptional = (targetClass == Optional.class);
177180
boolean isArgumentValue = (targetClass == ArgumentValue.class);
181+
boolean isFieldValue = (targetClass == FieldValue.class);
178182

179-
if (isOptional || isArgumentValue) {
183+
if (isOptional || isArgumentValue || isFieldValue) {
180184
targetType = targetType.getNested(2);
181185
targetClass = targetType.resolve();
182186
}
@@ -202,6 +206,9 @@ else if (rawValue instanceof Map) {
202206
else if (isArgumentValue) {
203207
value = (isOmitted ? ArgumentValue.omitted() : ArgumentValue.ofNullable(value));
204208
}
209+
else if (isFieldValue) {
210+
value = (isOmitted ? FieldValue.omitted() : FieldValue.ofNullable(value));
211+
}
205212

206213
return value;
207214
}

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.springframework.core.ResolvableType;
6262
import org.springframework.core.annotation.AnnotatedElementUtils;
6363
import org.springframework.data.domain.ScrollPosition;
64+
import org.springframework.graphql.FieldValue;
6465
import org.springframework.graphql.data.ArgumentValue;
6566
import org.springframework.graphql.data.GraphQlArgumentBinder;
6667
import org.springframework.graphql.data.method.HandlerMethod;
@@ -488,10 +489,12 @@ public ResolvableType getReturnType() {
488489
}
489490

490491
@Override
492+
@SuppressWarnings("removal")
491493
public Map<String, ResolvableType> getArguments() {
492494

493495
Predicate<MethodParameter> argumentPredicate = (p) ->
494-
(p.getParameterAnnotation(Argument.class) != null || p.getParameterType() == ArgumentValue.class);
496+
(p.getParameterAnnotation(Argument.class) != null || p.getParameterType() == ArgumentValue.class ||
497+
p.getParameterType() == FieldValue.class);
495498

496499
return Arrays.stream(this.mappingInfo.getHandlerMethod().getMethodParameters())
497500
.filter(argumentPredicate)

0 commit comments

Comments
 (0)