Refine GraphQlResponseField getError()
Instead of a simple check, looking for an associated field error at or above the field, this method now more focused on finding the reason for a failure when the field has no value. This allows performing a more thorough search including cases when the field error is at, above, or even below (e.g. non-null nested field that bubbled up), in the end falling back on request errors (e.g. failed response without any field errors). Also, rename ResponseField to GraphQlResponseField and move to a top-level class. See gh-10
This commit is contained in:
@@ -135,7 +135,7 @@ which is a strategy for loading the document for a request by file name.
|
||||
|
||||
Once you have a <<client-graphqlclient>>, you can begin to perform requests via
|
||||
<<client-requests-retrieve, retrieve()>> or <<client-requests-execute, execute()>>
|
||||
where the former is merely a shortcut for the latter.
|
||||
where the former is only a shortcut for the latter.
|
||||
|
||||
|
||||
|
||||
@@ -159,25 +159,21 @@ The below retrieves and decodes the data for a query:
|
||||
.retrieve("project") <2>
|
||||
.toEntity(Project.class); <3>
|
||||
----
|
||||
<1> The operation to perform
|
||||
<2> Specify a path under the "data" key in the response map
|
||||
<3> Decode the data at the path to the target type
|
||||
<1> The operation to perform.
|
||||
<2> The path under the "data" key in the response map to decode from.
|
||||
<3> Decode the data at the path to the target type.
|
||||
|
||||
The document is a `String` that could be a literal or produced through a code generated
|
||||
request object. You can also define documents in files and use a
|
||||
The input document is a `String` that could be a literal or produced through a code
|
||||
generated request object. You can also define documents in files and use a
|
||||
<<client-requests-document-source>> to resole them by file name.
|
||||
|
||||
The path is relative to the "data" key and uses a simple dot (".") separated notation
|
||||
for nested fields with optional array indices for list elements, e.g. `"project.name"`,
|
||||
`"project.releases[0].version"`, and so on.
|
||||
for nested fields with optional array indices for list elements, e.g. `"project.name"`
|
||||
or `"project.releases[0].version"`.
|
||||
|
||||
Decoding can fail with `FieldAccessException` if the given path is not present in the
|
||||
response map, or when the value is `null` and there is an error for the field.
|
||||
|
||||
By default, `FieldAccessException` is also raised on `retrieve` for partial data where
|
||||
the field value exists but nested fields may be `null` with a field error. In such
|
||||
cases, you can handle the exception to examine the errors and decide whether or how to
|
||||
decode the partial data:
|
||||
Decoding can result in `FieldAccessException` if the given path is not present, or the
|
||||
field value is `null` and has an error. `FieldAccessException` provides access to the
|
||||
response and the field:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -185,24 +181,20 @@ decode the partial data:
|
||||
.retrieve("project")
|
||||
.toEntity(Project.class)
|
||||
.onErrorResume(FieldAccessException.class, ex -> {
|
||||
ResponseField field = ex.getField();
|
||||
// Use field to check nested field errors and/or decode
|
||||
// Return Mono with Project or an error
|
||||
ClientGraphQlResponse response = ex.getResponse();
|
||||
// ...
|
||||
GraphQlResponseField field = ex.getField();
|
||||
// ...
|
||||
});
|
||||
----
|
||||
|
||||
TIP: The GraphQL spec considers a partial response or a partial field to be valid, and
|
||||
it may be feasible to decode them. By contrast, a failed field (i.e. value not present or
|
||||
is `null` with field error) or a failed response (no "data" key) are not valid and
|
||||
attempts to decode those are always rejected.
|
||||
|
||||
|
||||
|
||||
[[client-requests-execute]]
|
||||
=== Execute
|
||||
|
||||
The `retrieve` method is only a shortcut to decode from a single path to a higher level
|
||||
object. For more control and access to the response, use the `execute` method.
|
||||
`retrieve` is only a shortcut to decode from a single path in the response map. For more
|
||||
control, use the `execute` method and handle the response:
|
||||
|
||||
For example:
|
||||
|
||||
@@ -212,17 +204,27 @@ For example:
|
||||
Mono<Project> projectMono = graphQlClient.document(document)
|
||||
.execute()
|
||||
.map(response -> {
|
||||
// Check response.isValid(), getErrors()
|
||||
if (!response.isValid()) {
|
||||
// Request failure... <1>
|
||||
}
|
||||
|
||||
ResponseField field = response.field("project");
|
||||
// Check field.hasValue(), getError()
|
||||
if (!field.hasValue()) {
|
||||
if (field.getError() != null) {
|
||||
// Field failure... <2>
|
||||
}
|
||||
else {
|
||||
// Optional field set to null... <3>
|
||||
}
|
||||
}
|
||||
|
||||
return field.toEntity(Project.class)
|
||||
return field.toEntity(Project.class); <4>
|
||||
});
|
||||
----
|
||||
|
||||
You can use `execute` to check response errors, obtain different fields, check their
|
||||
field errors and nested field errors, and/or decode their values.
|
||||
<1> The response does not have data, only errors
|
||||
<2> Field that is `null` and has an associated error
|
||||
<3> Field that was set to `null` by its `DataFetcher`
|
||||
<4> Decode the data at the given path
|
||||
|
||||
|
||||
|
||||
@@ -272,7 +274,7 @@ You can use the `GraphQlClient` <<client-graphqlclient-builder>> to customize th
|
||||
== Subscriptions
|
||||
|
||||
For a subscription operation, call `retrieveSubscription` instead of `retrieve` to
|
||||
obtain a stream of responses rather than a single response:
|
||||
obtain a stream of responses, each decoded to a target object:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -281,29 +283,34 @@ obtain a stream of responses rather than a single response:
|
||||
.toEntity(String.class);
|
||||
----
|
||||
|
||||
Similar to the <<client-requests-retrieve>> vs <<client-requests-execute>> choice
|
||||
for requests with a single response, the same choice is also available for subscriptions.
|
||||
For example, for more control over each response, use `executeSubscription` instead of
|
||||
`retrieveSubscription`:
|
||||
Similar to the <<client-requests-retrieve, retrieve>> vs <<client-requests-execute, execute>>
|
||||
alternatives for single response requests, the same is also available for subscriptions.
|
||||
For more control over each response, use `executeSubscription`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Flux<String> greetingFlux = client.document("subscription { greetings }")
|
||||
.executeSubscription()
|
||||
.map(response -> {
|
||||
// Check response.isValid(), getErrors()
|
||||
if (!response.isValid()) {
|
||||
// Request failure...
|
||||
}
|
||||
|
||||
ResponseField field = response.field("greeting");
|
||||
// Check field.isValid(), getError()
|
||||
ResponseField field = response.field("project");
|
||||
if (!field.hasValue()) {
|
||||
if (field.getError() != null) {
|
||||
// Field failure...
|
||||
}
|
||||
else {
|
||||
// Optional field set to null... <3>
|
||||
}
|
||||
}
|
||||
|
||||
return field.toEntity(String.class)
|
||||
});
|
||||
----
|
||||
|
||||
|
||||
|
||||
Subscriptions are supported only with the <<client-websocketgraphqlclient,
|
||||
WebSocketGraphQlClient>> extension.
|
||||
NOTE: Subscriptions are supported only over <<client-websocketgraphqlclient, WebSocket>>.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import org.springframework.graphql.GraphQlResponse;
|
||||
public interface ClientGraphQlResponse extends GraphQlResponse {
|
||||
|
||||
/**
|
||||
* Return the request associated with this response.
|
||||
* Return the request for the response.
|
||||
*/
|
||||
GraphQlRequest getRequest();
|
||||
|
||||
@@ -48,10 +48,10 @@ public interface ClientGraphQlResponse extends GraphQlResponse {
|
||||
* </pre>
|
||||
* @param path relative to the "data" key
|
||||
* @return representation for the field with further options to inspect or
|
||||
* decode its value; use {@link ResponseField#hasValue()} to check if the
|
||||
* field actually exists and has a value.
|
||||
* decode its value; use {@link GraphQlResponseField#hasValue()} to check if
|
||||
* the field actually exists and has a value.
|
||||
*/
|
||||
ResponseField field(String path);
|
||||
GraphQlResponseField field(String path);
|
||||
|
||||
/**
|
||||
* Decode the full response map to the given target type.
|
||||
|
||||
@@ -23,19 +23,13 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.GraphQlResponse;
|
||||
import org.springframework.graphql.GraphQlResponseError;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
||||
@@ -70,13 +64,22 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C
|
||||
return this.request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseField field(String path) {
|
||||
List<Object> dataPath = parseFieldPath(path);
|
||||
return new DefaultField(path, dataPath, getFieldValue(dataPath), getFieldErrors(path));
|
||||
Encoder<?> getEncoder() {
|
||||
return this.encoder;
|
||||
}
|
||||
|
||||
private static List<Object> parseFieldPath(String path) {
|
||||
Decoder<?> getDecoder() {
|
||||
return this.decoder;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public GraphQlResponseField field(String path) {
|
||||
List<Object> parsedPath = parsePath(path);
|
||||
return new DefaultGraphQlResponseField(this, path, parsedPath, getValue(parsedPath), getFieldErrors(path));
|
||||
}
|
||||
|
||||
private static List<Object> parsePath(String path) {
|
||||
if (!StringUtils.hasText(path)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -117,18 +120,18 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object getFieldValue(List<Object> fieldPath) {
|
||||
private Object getValue(List<Object> path) {
|
||||
Object value = (isValid() ? getData() : null);
|
||||
for (Object segment : fieldPath) {
|
||||
for (Object segment : path) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (segment instanceof String) {
|
||||
Assert.isTrue(value instanceof Map, () -> "Invalid path " + fieldPath + ", data: " + getData());
|
||||
Assert.isTrue(value instanceof Map, () -> "Invalid path " + path + ", data: " + getData());
|
||||
value = ((Map<?, ?>) value).getOrDefault(segment, null);
|
||||
}
|
||||
else {
|
||||
Assert.isTrue(value instanceof List, () -> "Invalid path " + fieldPath + ", data: " + getData());
|
||||
Assert.isTrue(value instanceof List, () -> "Invalid path " + path + ", data: " + getData());
|
||||
int index = (int) segment;
|
||||
value = (index < ((List<?>) value).size() ? ((List<?>) value).get(index) : null);
|
||||
}
|
||||
@@ -163,102 +166,4 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C
|
||||
return field("").toEntity(type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ResponseField}.
|
||||
*/
|
||||
private class DefaultField implements ResponseField {
|
||||
|
||||
private final String path;
|
||||
|
||||
private final List<Object> parsedPath;
|
||||
|
||||
private final List<GraphQlResponseError> fieldErrors;
|
||||
|
||||
@Nullable
|
||||
private final Object value;
|
||||
|
||||
public DefaultField(
|
||||
String path, List<Object> parsedPath, @Nullable Object value, List<GraphQlResponseError> errors) {
|
||||
|
||||
this.path = path;
|
||||
this.parsedPath = parsedPath;
|
||||
this.value = value;
|
||||
this.fieldErrors = errors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Object> getParsedPath() {
|
||||
return this.parsedPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasValue() {
|
||||
return (this.value != null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T getValue() {
|
||||
return (T) this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlResponseError getError() {
|
||||
for (GraphQlResponseError error : this.fieldErrors) {
|
||||
if (error.getParsedPath().size() <= this.parsedPath.size()) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GraphQlResponseError> getErrors() {
|
||||
return this.fieldErrors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> D toEntity(Class<D> entityType) {
|
||||
return toEntity(ResolvableType.forType(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> D toEntity(ParameterizedTypeReference<D> entityType) {
|
||||
return toEntity(ResolvableType.forType(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(Class<D> elementType) {
|
||||
return toEntity(ResolvableType.forClassWithGenerics(List.class, elementType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(ParameterizedTypeReference<D> elementType) {
|
||||
return toEntity(ResolvableType.forClassWithGenerics(List.class, ResolvableType.forType(elementType)));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "ConstantConditions"})
|
||||
private <T> T toEntity(ResolvableType targetType) {
|
||||
if (this.value == null) {
|
||||
throw new FieldAccessException(request, DefaultClientGraphQlResponse.this, this);
|
||||
}
|
||||
|
||||
DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
|
||||
MimeType mimeType = MimeTypeUtils.APPLICATION_JSON;
|
||||
Map<String, Object> hints = Collections.emptyMap();
|
||||
|
||||
DataBuffer buffer = ((Encoder<T>) encoder).encodeValue(
|
||||
(T) this.value, bufferFactory, ResolvableType.forInstance(this.value), mimeType, hints);
|
||||
|
||||
return ((Decoder<T>) decoder).decode(buffer, targetType, mimeType, hints);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -199,14 +199,14 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the field if valid, possibly {@code null}.
|
||||
* @throws FieldAccessException if the response or field is not valid
|
||||
* Return the field if valid, or {@code null} if {@code null} without errors.
|
||||
* @throws FieldAccessException for invalid response or failed field
|
||||
*/
|
||||
@Nullable
|
||||
protected ResponseField getValidField(ClientGraphQlResponse response) {
|
||||
ResponseField field = response.field(this.path);
|
||||
protected GraphQlResponseField getValidField(ClientGraphQlResponse response) {
|
||||
GraphQlResponseField field = response.field(this.path);
|
||||
if (!response.isValid() || field.getError() != null) {
|
||||
throw new FieldAccessException(response.getRequest(), response, field);
|
||||
throw new FieldAccessException(response, field);
|
||||
}
|
||||
return (field.hasValue() ? field : null);
|
||||
}
|
||||
@@ -236,7 +236,7 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
@Override
|
||||
public <D> Mono<List<D>> toEntityList(Class<D> elementType) {
|
||||
return this.responseMono.map(response -> {
|
||||
ResponseField field = getValidField(response);
|
||||
GraphQlResponseField field = getValidField(response);
|
||||
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
|
||||
});
|
||||
}
|
||||
@@ -244,7 +244,7 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
@Override
|
||||
public <D> Mono<List<D>> toEntityList(ParameterizedTypeReference<D> elementType) {
|
||||
return this.responseMono.map(response -> {
|
||||
ResponseField field = getValidField(response);
|
||||
GraphQlResponseField field = getValidField(response);
|
||||
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
|
||||
});
|
||||
}
|
||||
@@ -274,7 +274,7 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
@Override
|
||||
public <D> Flux<List<D>> toEntityList(Class<D> elementType) {
|
||||
return this.responseFlux.map(response -> {
|
||||
ResponseField field = getValidField(response);
|
||||
GraphQlResponseField field = getValidField(response);
|
||||
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
|
||||
});
|
||||
}
|
||||
@@ -282,7 +282,7 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
@Override
|
||||
public <D> Flux<List<D>> toEntityList(ParameterizedTypeReference<D> elementType) {
|
||||
return this.responseFlux.map(response -> {
|
||||
ResponseField field = getValidField(response);
|
||||
GraphQlResponseField field = getValidField(response);
|
||||
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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.client;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.graphql.GraphQlResponseError;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@link GraphQlResponseField}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class DefaultGraphQlResponseField implements GraphQlResponseField {
|
||||
|
||||
private final DefaultClientGraphQlResponse response;
|
||||
|
||||
private final String path;
|
||||
|
||||
private final List<Object> parsedPath;
|
||||
|
||||
@Nullable
|
||||
private final Object value;
|
||||
|
||||
private final List<GraphQlResponseError> fieldErrors;
|
||||
|
||||
|
||||
DefaultGraphQlResponseField(
|
||||
DefaultClientGraphQlResponse response, String path, List<Object> parsedPath,
|
||||
@Nullable Object value, List<GraphQlResponseError> errors) {
|
||||
|
||||
this.response = response;
|
||||
this.path = path;
|
||||
this.parsedPath = parsedPath;
|
||||
this.value = value;
|
||||
this.fieldErrors = errors;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Object> getParsedPath() {
|
||||
return this.parsedPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasValue() {
|
||||
return (this.value != null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T getValue() {
|
||||
return (T) this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlResponseError getError() {
|
||||
if (!hasValue()) {
|
||||
if (!this.fieldErrors.isEmpty()) {
|
||||
return this.fieldErrors.get(0);
|
||||
}
|
||||
if (!this.response.getErrors().isEmpty()) {
|
||||
return this.response.getErrors().get(0);
|
||||
}
|
||||
// No errors, set to null by DataFetcher
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GraphQlResponseError> getErrors() {
|
||||
return this.fieldErrors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> D toEntity(Class<D> entityType) {
|
||||
return toEntity(ResolvableType.forType(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> D toEntity(ParameterizedTypeReference<D> entityType) {
|
||||
return toEntity(ResolvableType.forType(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(Class<D> elementType) {
|
||||
return toEntity(ResolvableType.forClassWithGenerics(List.class, elementType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(ParameterizedTypeReference<D> elementType) {
|
||||
return toEntity(ResolvableType.forClassWithGenerics(List.class, ResolvableType.forType(elementType)));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "ConstantConditions"})
|
||||
private <T> T toEntity(ResolvableType targetType) {
|
||||
if (this.value == null) {
|
||||
throw new FieldAccessException(this.response, this);
|
||||
}
|
||||
|
||||
DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
|
||||
MimeType mimeType = MimeTypeUtils.APPLICATION_JSON;
|
||||
Map<String, Object> hints = Collections.emptyMap();
|
||||
|
||||
DataBuffer buffer = ((Encoder<T>) this.response.getEncoder()).encodeValue(
|
||||
(T) this.value, bufferFactory, ResolvableType.forInstance(this.value), mimeType, hints);
|
||||
|
||||
return ((Decoder<T>) this.response.getDecoder()).decode(buffer, targetType, mimeType, hints);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,13 +17,12 @@
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.GraphQlResponse;
|
||||
|
||||
/**
|
||||
* An exception raised on an attempt to decode data from a
|
||||
* {@link GraphQlResponse#isValid() failed response} or a field is not present,
|
||||
* or has no value, checked via {@link ResponseField#hasValue()}.
|
||||
* or has no value, checked via {@link GraphQlResponseField#hasValue()}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
@@ -33,19 +32,19 @@ public class FieldAccessException extends GraphQlClientException {
|
||||
|
||||
private final ClientGraphQlResponse response;
|
||||
|
||||
private final ResponseField field;
|
||||
private final GraphQlResponseField field;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with the request and response, and the accessed field.
|
||||
*/
|
||||
public FieldAccessException(GraphQlRequest request, ClientGraphQlResponse response, ResponseField field) {
|
||||
super(initDefaultMessage(field), null, request);
|
||||
public FieldAccessException(ClientGraphQlResponse response, GraphQlResponseField field) {
|
||||
super(initDefaultMessage(field), null, response.getRequest());
|
||||
this.response = response;
|
||||
this.field = field;
|
||||
}
|
||||
|
||||
private static String initDefaultMessage(ResponseField field) {
|
||||
private static String initDefaultMessage(GraphQlResponseField field) {
|
||||
return "Invalid field '" + field.getPath() + "', errors: " + field.getErrors();
|
||||
}
|
||||
|
||||
@@ -60,7 +59,7 @@ public class FieldAccessException extends GraphQlClientException {
|
||||
/**
|
||||
* Return the field that needed to be accessed.
|
||||
*/
|
||||
public ResponseField getField() {
|
||||
public GraphQlResponseField getField() {
|
||||
return this.field;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.GraphQlResponse;
|
||||
import org.springframework.graphql.support.DocumentSource;
|
||||
import org.springframework.graphql.support.ResourceDocumentSource;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -180,10 +181,11 @@ public interface GraphQlClient {
|
||||
/**
|
||||
* Decode the field to an entity of the given type.
|
||||
* @param entityType the type to convert to
|
||||
* @return {@code Mono} that provides the decoded entity, or completes
|
||||
* empty when the field is {@code null} but without errors, or ends with
|
||||
* a {@link FieldAccessException} if the target field is not present or
|
||||
* has no value.
|
||||
* @return {@code Mono} with the decoded entity. Completes empty when
|
||||
* the field is {@code null} without errors, or ends with
|
||||
* {@link FieldAccessException} for an invalid response or a failed field
|
||||
* @see GraphQlResponse#isValid()
|
||||
* @see GraphQlResponseField#getError()
|
||||
*/
|
||||
<D> Mono<D> toEntity(Class<D> entityType);
|
||||
|
||||
@@ -198,6 +200,8 @@ public interface GraphQlClient {
|
||||
* @return {@code Mono} with a list of decoded entities, possibly an
|
||||
* empty list, or ends with {@link FieldAccessException} if the target
|
||||
* field is not present or has no value.
|
||||
* @see GraphQlResponse#isValid()
|
||||
* @see GraphQlResponseField#getError()
|
||||
*/
|
||||
<D> Mono<List<D>> toEntityList(Class<D> elementType);
|
||||
|
||||
@@ -217,11 +221,12 @@ public interface GraphQlClient {
|
||||
/**
|
||||
* Decode the field to an entity of the given type.
|
||||
* @param entityType the type to convert to
|
||||
* @return decoded entities, one for each response, except responses
|
||||
* in which the field is {@code null} but without errors, or ending with
|
||||
* {@link FieldAccessException} if the target field is not present or
|
||||
* has no value in a given response; the stream may also end with a
|
||||
* {@link GraphQlTransportException}.
|
||||
* @return a stream of decoded entities, one for each response, excluding
|
||||
* responses in which the field is {@code null} without errors. Ends with
|
||||
* {@link FieldAccessException} for an invalid response or a failed field.
|
||||
* May also end with a {@link GraphQlTransportException}.
|
||||
* @see GraphQlResponse#isValid()
|
||||
* @see GraphQlResponseField#getError()
|
||||
*/
|
||||
<D> Flux<D> toEntity(Class<D> entityType);
|
||||
|
||||
@@ -233,11 +238,12 @@ public interface GraphQlClient {
|
||||
/**
|
||||
* Decode the field to a list of entities with the given type.
|
||||
* @param elementType the type of elements in the list
|
||||
* @return lists of decoded entities, one for each response, except responses
|
||||
* in which the field is {@code null} but without errors, or ending with
|
||||
* {@link FieldAccessException} if the target field is not present or
|
||||
* has no value in a given response; the stream may also end with a
|
||||
* {@link GraphQlTransportException}.
|
||||
* @return lists of decoded entities, one for each response, excluding
|
||||
* responses in which the field is {@code null} without errors. Ends with
|
||||
* {@link FieldAccessException} for an invalid response or a failed field.
|
||||
* May also end with a {@link GraphQlTransportException}.
|
||||
* @see GraphQlResponse#isValid()
|
||||
* @see GraphQlResponseField#getError()
|
||||
*/
|
||||
<D> Flux<List<D>> toEntityList(Class<D> elementType);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.springframework.graphql.client;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.GraphQlResponse;
|
||||
import org.springframework.graphql.GraphQlResponseError;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -30,18 +31,15 @@ import org.springframework.lang.Nullable;
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface ResponseField {
|
||||
public interface GraphQlResponseField {
|
||||
|
||||
/**
|
||||
* Whether the field has a value.
|
||||
* <ul>
|
||||
* <li>{@code "true"} means the field is not {@code null} in which case there
|
||||
* is no field {@link #getError() error}. The field may still be partial and
|
||||
* have nested, field {@link #getErrors() errors}.
|
||||
* <li>{@code "false"} means the field is {@code null} or does not exist.
|
||||
* Check for a field {@link #getError()}, which may be on the field or on a
|
||||
* parent field. The field may also be {@code null} because it is defined as
|
||||
* optional in the schema.
|
||||
* <li>{@code "true"} means the field is not {@code null}, and therefore valid,
|
||||
* although it may be partial with nested field {@link #getErrors() errors}.
|
||||
* <li>{@code "false"} means the field is {@code null} or doesn't exist; use
|
||||
* {@link #getError()} to check if the field is {@code null} due to an error.
|
||||
* </ul>
|
||||
*/
|
||||
boolean hasValue();
|
||||
@@ -68,13 +66,23 @@ public interface ResponseField {
|
||||
<T> T getValue();
|
||||
|
||||
/**
|
||||
* Return the error for this field, if any. The error may be for this field
|
||||
* when the field is {@code null}, or it may be for a parent field, when the
|
||||
* current field does not exist.
|
||||
* <p><strong>Note:</strong> The field error is identified by searching for
|
||||
* the first error with a matching path that is shorter or the same as the
|
||||
* field path. According to the GraphQL spec, section 6.4.4,
|
||||
* "Handling Field Errors", there should be only one field error per field.
|
||||
* Return the error that provides the reason for a failed field.
|
||||
* <p>When the field <strong>does not</strong> {@link #hasValue() have} a
|
||||
* value, this method looks for the first field error. According to the
|
||||
* GraphQL spec, section 6.4.4, "Handling Field Errors", there should be
|
||||
* only one error per field. The returned field error may be:
|
||||
* <ul>
|
||||
* <li>on the field
|
||||
* <li>on a parent field, when the field is not present
|
||||
* <li>on a nested field, when a {@code non-null} nested field error bubbles up
|
||||
* </ul>
|
||||
* <p>As a fallback, this method also checks "request errors" in case the
|
||||
* entire response is not {@link GraphQlResponse#isValid() valid}. If there
|
||||
* are no errors at all, {@code null} is returned, and it implies the field
|
||||
* value was set to {@code null} by its {@code DataFetcher}.
|
||||
* <p>When the field <strong>does</strong> have a value, it is considered
|
||||
* valid and this method returns {@code null}, although the field may be
|
||||
* partial and contain {@link #getErrors() errors} on nested fields.
|
||||
* @return return the error for this field, or {@code null} if there is no
|
||||
* error with the same path as the field path
|
||||
*/
|
||||
@@ -83,9 +91,10 @@ public interface ResponseField {
|
||||
|
||||
/**
|
||||
* Return all field errors including errors above, at, and below this field.
|
||||
* <p>In practice, when the field has a value, all errors are for fields
|
||||
* below. When the field does not have a value, there is only one error, and
|
||||
* it is the same as {@link #getError()}.
|
||||
* <p>In practice, when the field <strong>does have</strong> a value, it is
|
||||
* considered valid but possibly partial with nested field errors. When the
|
||||
* field <strong>does not have</strong> a value, there should be only one
|
||||
* field error, and in that case it is better to use {@link #getError()}.
|
||||
*/
|
||||
List<GraphQlResponseError> getErrors();
|
||||
|
||||
@@ -127,7 +127,7 @@ public class DefaultGraphQlClientResponseTests {
|
||||
GraphQLError error2 = createError("/me/friends", "fail-me-friends");
|
||||
GraphQLError error3 = createError("/me/friends[0]/name", "fail-me-friends-name");
|
||||
|
||||
ResponseField field = getField(path, error0, error1, error2, error3);
|
||||
GraphQlResponseField field = getField(path, error0, error1, error2, error3);
|
||||
List<GraphQlResponseError> errors = field.getErrors();
|
||||
|
||||
assertThat(errors).hasSize(3);
|
||||
@@ -144,13 +144,13 @@ public class DefaultGraphQlClientResponseTests {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private ResponseField getField(String path, String dataJson) throws Exception {
|
||||
private GraphQlResponseField getField(String path, String dataJson) throws Exception {
|
||||
Map<?, ?> dataMap = mapper.readValue(dataJson, Map.class);
|
||||
ClientGraphQlResponse response = creatResponse(Collections.singletonMap("data", dataMap));
|
||||
return response.field(path);
|
||||
}
|
||||
|
||||
private ResponseField getField(String path, GraphQLError... errors) {
|
||||
private GraphQlResponseField getField(String path, GraphQLError... errors) {
|
||||
List<?> list = Arrays.stream(errors).map(GraphQLError::toSpecification).collect(Collectors.toList());
|
||||
ClientGraphQlResponse response = creatResponse(Collections.singletonMap("errors", list));
|
||||
return response.field(path);
|
||||
|
||||
@@ -188,7 +188,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
|
||||
.as("Partial response with field errors should be considered valid")
|
||||
.isTrue();
|
||||
|
||||
ResponseField field = response.field("me");
|
||||
GraphQlResponseField field = response.field("me");
|
||||
assertThat(field.hasValue()).isTrue();
|
||||
assertThat(field.getErrors()).hasSize(1);
|
||||
assertThat(field.getErrors().get(0).getParsedPath()).containsExactly("me", "name");
|
||||
@@ -196,7 +196,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
|
||||
.as("Decoding with nested field error should not be precluded")
|
||||
.isNotNull();
|
||||
|
||||
ResponseField nameField = response.field("me.name");
|
||||
GraphQlResponseField nameField = response.field("me.name");
|
||||
assertThat(nameField.hasValue()).isFalse();
|
||||
assertThat(nameField.getError()).isNotNull();
|
||||
assertThat(nameField.getError().getParsedPath()).containsExactly("me", "name");
|
||||
@@ -204,7 +204,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
|
||||
.as("Decoding field null with direct field error should be rejected")
|
||||
.isInstanceOf(FieldAccessException.class);
|
||||
|
||||
ResponseField nonExistingField = response.field("me.name.other");
|
||||
GraphQlResponseField nonExistingField = response.field("me.name.other");
|
||||
assertThat(nonExistingField.hasValue()).isFalse();
|
||||
assertThat(nameField.getError()).isNotNull();
|
||||
assertThat(nameField.getError().getParsedPath()).containsExactly("me", "name");
|
||||
|
||||
Reference in New Issue
Block a user