Skip to content

Commit 966ba36

Browse files
rstoyanchevjosianyman
authored andcommitted
Add support for checking if an argument was omitted
Closes spring-projectsgh-518
1 parent 9ff6bae commit 966ba36

File tree

8 files changed

+459
-201
lines changed

8 files changed

+459
-201
lines changed

spring-graphql-docs/src/docs/asciidoc/index.adoc

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,14 +1290,19 @@ Schema mapping handler methods can have any of the following method arguments:
12901290
| For access to a named field argument bound to a higher-level, typed Object.
12911291
See <<controllers-schema-mapping-argument>>.
12921292

1293-
| `@Arguments`
1294-
| For access to all field arguments bound to a higher-level, typed Object.
1295-
See <<controllers-schema-mapping-arguments>>.
1296-
12971293
| `@Argument Map<String, Object>`
12981294
| For access to the raw map of arguments, where `@Argument` does not have a
12991295
`name` attribute.
13001296

1297+
| `ArgumentValue`
1298+
| For access to a named field argument bound to a higher-level, typed Object along
1299+
with a flag to indicate if the input argument was omitted vs set to `null`.
1300+
See <<controllers-schema-mapping-argument-value>>.
1301+
1302+
| `@Arguments`
1303+
| For access to all field arguments bound to a higher-level, typed Object.
1304+
See <<controllers-schema-mapping-arguments>>.
1305+
13011306
| `@Arguments Map<String, Object>`
13021307
| For access to the raw map of arguments.
13031308

@@ -1348,7 +1353,6 @@ Schema mapping handler methods can return:
13481353
For this to work, `AnnotatedControllerConfigurer` must be configured with an `Executor`.
13491354

13501355

1351-
13521356
[[controllers-schema-mapping-argument]]
13531357
==== `@Argument`
13541358

@@ -1395,6 +1399,43 @@ You can use `@Argument` with a `Map<String, Object>` argument, to obtain the raw
13951399
all argument values. The name attribute on `@Argument` must not be set.
13961400

13971401

1402+
[[controllers-schema-mapping-argument-value]]
1403+
==== `ArgumentValue`
1404+
1405+
By default, input arguments in GraphQL are nullable and optional, which means an argument
1406+
can be set to the `null` literal, or not provided at all. This distinction is useful for
1407+
partial updates with a mutation where the underlying data may also be, either set to
1408+
`null` or not changed at all accordingly. When using <<controllers-schema-mapping-argument>>
1409+
there is no way to make such a distinction, because you would get `null` or an empty
1410+
`Optional` in both cases.
1411+
1412+
If you want to know not whether a value was not provided at all, you can declare an
1413+
`ArgumentValue` method parameter, which is a simple container for the resulting value,
1414+
along with a flag to indicate whether the input argument was omitted altogether. You
1415+
can use this instead of `@Argument`, in which case the argument name is determined from
1416+
the method parameter name, or together with `@Argument` to specify the argument name.
1417+
1418+
For example:
1419+
1420+
[source,java,indent=0,subs="verbatim,quotes"]
1421+
----
1422+
@Controller
1423+
public class BookController {
1424+
1425+
@MutationMapping
1426+
public void addBook(ArgumentValue<BookInput> bookInput) {
1427+
if (!bookInput.isOmitted) {
1428+
BookInput value = bookInput.value();
1429+
// ...
1430+
}
1431+
}
1432+
}
1433+
----
1434+
1435+
`ArgumentValue` is also supported as a field within the object structure of an `@Argument`
1436+
method parameter, either initialized via a constructor argument or via a setter, including
1437+
as a field of an object nested at any level below the top level object.
1438+
13981439

13991440
[[controllers-schema-mapping-arguments]]
14001441
==== `@Arguments`
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2002-2022 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+
package org.springframework.graphql.data;
17+
18+
19+
import java.util.Optional;
20+
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.util.ObjectUtils;
23+
24+
/**
25+
* Simple container for the value from binding a GraphQL argument to a higher
26+
* level Object, along with a flag to indicate whether the input argument was
27+
* omitted altogether, as opposed to provided but set to the {@literal "null"}
28+
* literal.
29+
*
30+
* <p>Supported in one of the following places:
31+
* <ul>
32+
* <li>On a controller method parameter, either instead of
33+
* {@link org.springframework.graphql.data.method.annotation.Argument @Argument}
34+
* in which case the argument name is determined from the method parameter name,
35+
* or together with {@code @Argument} to specify the argument name.
36+
* <li>As a field within the object structure of an {@code @Argument} method
37+
* parameter, either initialized via a constructor argument or a setter,
38+
* including as a field of an object nested at any level below the top level
39+
* object.
40+
* </ul>
41+
*
42+
* @author Rossen Stoyanchev
43+
* @param <T> the type of value contained
44+
* @since 1.1
45+
* @see <a href="http://spec.graphql.org/October2021/#sec-Non-Null.Nullable-vs-Optional">Nullable vs Optional</a>
46+
*/
47+
public final class ArgumentValue<T> {
48+
49+
private static final ArgumentValue<?> OMITTED = new ArgumentValue<>(null, false);
50+
51+
52+
@Nullable
53+
private final T value;
54+
55+
private final boolean omitted;
56+
57+
58+
private ArgumentValue(@Nullable T value, boolean omitted) {
59+
this.value = value;
60+
this.omitted = omitted;
61+
}
62+
63+
64+
/**
65+
* Return {@code true} if a non-null value is present, and {@code false} otherwise.
66+
*/
67+
public boolean isPresent() {
68+
return (this.value != null);
69+
}
70+
71+
/**
72+
* Return {@code true} if the input value was omitted altogether from the
73+
* input, and {@code false} if it was provided, but possibly set to the
74+
* {@literal "null"} literal.
75+
*/
76+
public boolean isOmitted() {
77+
return this.omitted;
78+
}
79+
80+
/**
81+
* Return the contained value, or {@code null}.
82+
*/
83+
@Nullable
84+
public T value() {
85+
return this.value;
86+
}
87+
88+
/**
89+
* Return the contained value as a nullable {@link Optional}.
90+
*/
91+
public Optional<T> asOptional() {
92+
return Optional.ofNullable(this.value);
93+
}
94+
95+
@Override
96+
public boolean equals(Object other) {
97+
// This covers OMITTED constant
98+
if (this == other) {
99+
return true;
100+
}
101+
if (!(other instanceof ArgumentValue<?> otherValue)) {
102+
return false;
103+
}
104+
return ObjectUtils.nullSafeEquals(this.value, otherValue.value);
105+
}
106+
107+
@Override
108+
public int hashCode() {
109+
int result = ObjectUtils.nullSafeHashCode(this.value);
110+
result = 31 * result + Boolean.hashCode(this.omitted);
111+
return result;
112+
}
113+
114+
115+
/**
116+
* Static factory method for an argument value that was provided, even if
117+
* it was set to {@literal "null}.
118+
* @param value the value to hold in the instance
119+
*/
120+
public static <T> ArgumentValue<T> ofNullable(@Nullable T value) {
121+
return new ArgumentValue<>(value, false);
122+
}
123+
124+
/**
125+
* Static factory method for an argument value that was omitted.
126+
*/
127+
@SuppressWarnings("unchecked")
128+
public static <T> ArgumentValue<T> omitted() {
129+
return (ArgumentValue<T>) OMITTED;
130+
}
131+
132+
}

0 commit comments

Comments
 (0)