Add RetrieveSpec to GraphQlClient

Provides a shortcut alternative to the execute methods, for decoding
a single field from the response.

See gh-10
This commit is contained in:
rstoyanchev
2022-03-15 07:44:31 +00:00
parent 38a9eb1b3c
commit e7b2f72d55
11 changed files with 414 additions and 124 deletions

View File

@@ -23,6 +23,7 @@ import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
* Represents a GraphQL request with the inputs to pass to a GraphQL service
@@ -63,7 +64,7 @@ public class GraphQlRequest {
Assert.notNull(document, "'document' is required");
this.document = document;
this.operationName = operationName;
this.variables = ((variables != null) ? variables : Collections.emptyMap());
this.variables = (variables != null ? variables : Collections.emptyMap());
}
@@ -115,6 +116,24 @@ public class GraphQlRequest {
return map;
}
@Override
public boolean equals(Object o) {
if (! (o instanceof GraphQlRequest)) {
return false;
}
GraphQlRequest other = (GraphQlRequest) o;
return (getDocument().equals(other.getDocument()) &&
ObjectUtils.nullSafeEquals(getOperationName(), other.getOperationName()) &&
ObjectUtils.nullSafeEquals(getVariables(), other.getVariables()));
}
@Override
public int hashCode() {
return this.document.hashCode() +
31 * ObjectUtils.nullSafeHashCode(this.operationName) +
31 * this.variables.hashCode();
}
@Override
public String toString() {
return "document='" + getDocument() + "'" +

View File

@@ -42,11 +42,11 @@ public abstract class AbstractDelegatingGraphQlClient implements GraphQlClient {
}
public Request document(String document) {
public RequestSpec document(String document) {
return this.graphQlClient.document(document);
}
public Request documentName(String name) {
public RequestSpec documentName(String name) {
return this.graphQlClient.documentName(name);
}

View File

@@ -18,6 +18,7 @@ package org.springframework.graphql.client;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.lang.Nullable;
@@ -30,6 +31,11 @@ import org.springframework.lang.Nullable;
*/
public interface ClientGraphQlResponse extends GraphQlResponse {
/**
* Return the request associated with this response.
*/
GraphQlRequest getRequest();
/**
* Navigate to the given path under the "data" key of the response map where
* the path is a dot-separated string with optional array indexes.
@@ -44,7 +50,7 @@ public interface ClientGraphQlResponse extends GraphQlResponse {
* @param path relative to the "data" key
* @return representation for the field with further options to inspect or
* decode its value; use {@link ResponseField#isValid()} to check if the
* field actually exists and its value is present
* field actually exists, has a value, or field errors
*/
ResponseField field(String path);

View File

@@ -63,6 +63,11 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C
}
@Override
public GraphQlRequest getRequest() {
return this.request;
}
@Override
public ResponseField field(String path) {

View File

@@ -16,12 +16,14 @@
package org.springframework.graphql.client;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.graphql.GraphQlRequest;
@@ -69,13 +71,13 @@ final class DefaultGraphQlClient implements GraphQlClient {
@Override
public Request document(String document) {
return new DefaultRequest(Mono.just(document));
public RequestSpec document(String document) {
return new DefaultRequestSpec(Mono.just(document));
}
@Override
public Request documentName(String name) {
return new DefaultRequest(this.documentSource.getDocument(name));
public RequestSpec documentName(String name) {
return new DefaultRequestSpec(this.documentSource.getDocument(name));
}
@Override
@@ -107,9 +109,9 @@ final class DefaultGraphQlClient implements GraphQlClient {
/**
* Default {@link GraphQlClient.Request} implementation.
* Default {@link RequestSpec} implementation.
*/
private final class DefaultRequest implements Request {
private final class DefaultRequestSpec implements RequestSpec {
private final Mono<String> documentMono;
@@ -118,29 +120,39 @@ final class DefaultGraphQlClient implements GraphQlClient {
private final Map<String, Object> variables = new LinkedHashMap<>();
DefaultRequest(Mono<String> documentMono) {
DefaultRequestSpec(Mono<String> documentMono) {
Assert.notNull(documentMono, "'document' is required");
this.documentMono = documentMono;
}
@Override
public DefaultRequest operationName(@Nullable String operationName) {
public DefaultRequestSpec operationName(@Nullable String operationName) {
this.operationName = operationName;
return this;
}
@Override
public DefaultRequest variable(String name, Object value) {
public DefaultRequestSpec variable(String name, @Nullable Object value) {
this.variables.put(name, value);
return this;
}
@Override
public Request variables(Map<String, Object> variables) {
public RequestSpec variables(Map<String, Object> variables) {
this.variables.putAll(variables);
return this;
}
@Override
public RetrieveSpec retrieve(String path) {
return new DefaultRetrieveSpec(execute(), path);
}
@Override
public RetrieveSubscriptionSpec retrieveSubscription(String path) {
return new DefaultRetrieveSubscriptionSpec(executeSubscription(), path);
}
@Override
public Mono<ClientGraphQlResponse> execute() {
return initRequest().flatMap(request ->
@@ -177,4 +189,87 @@ final class DefaultGraphQlClient implements GraphQlClient {
}
private static class RetrieveSpecSupport {
private final String path;
protected RetrieveSpecSupport(String path) {
this.path = path;
}
protected ResponseField getField(ClientGraphQlResponse response) {
ResponseField field = response.field(this.path);
if (!field.isValid() || !field.getErrors().isEmpty()) {
GraphQlRequest request = response.getRequest();
throw new FieldAccessException(request, response, field);
}
return field;
}
}
private static class DefaultRetrieveSpec extends RetrieveSpecSupport implements RetrieveSpec {
private final Mono<ClientGraphQlResponse> responseMono;
DefaultRetrieveSpec(Mono<ClientGraphQlResponse> responseMono, String path) {
super(path);
this.responseMono = responseMono;
}
@Override
public <D> Mono<D> toEntity(Class<D> entityType) {
return this.responseMono.map(this::getField).mapNotNull(field -> field.toEntity(entityType));
}
@Override
public <D> Mono<D> toEntity(ParameterizedTypeReference<D> entityType) {
return this.responseMono.map(this::getField).mapNotNull(field -> field.toEntity(entityType));
}
@Override
public <D> Mono<List<D>> toEntityList(Class<D> elementType) {
return this.responseMono.map(this::getField).map(field -> field.toEntityList(elementType));
}
@Override
public <D> Mono<List<D>> toEntityList(ParameterizedTypeReference<D> elementType) {
return this.responseMono.map(this::getField).map(field -> field.toEntityList(elementType));
}
}
private static class DefaultRetrieveSubscriptionSpec extends RetrieveSpecSupport implements RetrieveSubscriptionSpec {
private final Flux<ClientGraphQlResponse> responseFlux;
DefaultRetrieveSubscriptionSpec(Flux<ClientGraphQlResponse> responseFlux, String path) {
super(path);
this.responseFlux = responseFlux;
}
@Override
public <D> Flux<D> toEntity(Class<D> entityType) {
return this.responseFlux.map(this::getField).mapNotNull(field -> field.toEntity(entityType));
}
@Override
public <D> Flux<D> toEntity(ParameterizedTypeReference<D> entityType) {
return this.responseFlux.map(this::getField).mapNotNull(field -> field.toEntity(entityType));
}
@Override
public <D> Flux<List<D>> toEntityList(Class<D> elementType) {
return this.responseFlux.map(this::getField).map(field -> field.toEntityList(elementType));
}
@Override
public <D> Flux<List<D>> toEntityList(ParameterizedTypeReference<D> elementType) {
return this.responseFlux.map(this::getField).map(field -> field.toEntityList(elementType));
}
}
}

View File

@@ -15,11 +15,13 @@
*/
package org.springframework.graphql.client;
import java.util.List;
import java.util.Map;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.graphql.support.ResourceDocumentSource;
import org.springframework.lang.Nullable;
@@ -50,7 +52,7 @@ public interface GraphQlClient {
* @param document the document for the request
* @return spec to further define or execute the request
*/
Request document(String document);
RequestSpec document(String document);
/**
* Variant of {@link #document(String)} that uses the given key to resolve
@@ -58,7 +60,7 @@ public interface GraphQlClient {
* {@link DocumentSource} that the client is configured with.
* @throws IllegalArgumentException if the content could not be loaded
*/
Request documentName(String name);
RequestSpec documentName(String name);
/**
* Return a builder initialized from the configuration of "this" client
@@ -103,7 +105,7 @@ public interface GraphQlClient {
/**
* Declare options to gather input for a GraphQL request and execute it.
*/
interface Request {
interface RequestSpec {
/**
* Set the name of the operation in the {@link #document(String) document}
@@ -111,7 +113,7 @@ public interface GraphQlClient {
* @param operationName the operation name
* @return this request spec
*/
Request operationName(@Nullable String operationName);
RequestSpec operationName(@Nullable String operationName);
/**
* Add a value for a variable defined by the operation.
@@ -119,18 +121,32 @@ public interface GraphQlClient {
* @param value the variable value
* @return this request spec
*/
Request variable(String name, Object value);
RequestSpec variable(String name, @Nullable Object value);
/**
* Add all given values for variables defined by the operation.
* @param variables the variable values
* @return this request spec
*/
Request variables(Map<String, Object> variables);
RequestSpec variables(Map<String, Object> variables);
/**
* Execute as a request with a single response such as a "query" or
* "mutation" operation.
* Shortcut for {@link #execute()} with a single field path to decode from.
* @return a spec with decoding options
* @throws FieldAccessException if the target field has any errors,
* including nested errors.
*/
RetrieveSpec retrieve(String path);
/**
* Shortcut for {@link #executeSubscription()} with a single field path to decode from.
* @return a spec with decoding options
*/
RetrieveSubscriptionSpec retrieveSubscription(String path);
/**
* Execute request with a single response, e.g. "query" or "mutation", and
* return a response for further options.
* @return a {@code Mono} with a {@code ClientGraphQlResponse} for further
* decoding of the response. The {@code Mono} may end wth an error due
* to transport level issues.
@@ -138,7 +154,7 @@ public interface GraphQlClient {
Mono<ClientGraphQlResponse> execute();
/**
* Execute a "subscription" request with a stream of responses.
* Execute a "subscription" request and return a stream of responses.
* @return a {@code Flux} with a {@code ClientGraphQlResponse} for further
* decoding of the response. The {@code Flux} may terminate as follows:
* <ul>
@@ -155,4 +171,86 @@ public interface GraphQlClient {
}
/**
* Declares options to decode a field for a single response operation.
*/
interface RetrieveSpec {
/**
* Decode the field to an entity of the given type.
* @param entityType the type to convert to
* @return {@code Mono} with the decoded entity, possibly empty if the field
* {@link ResponseField#getValue() value} is {@code null}
* @throws FieldAccessException if the target field is not
* {@link ResponseField#isValid() valid} or has any errors, including
* nested errors.
*/
<D> Mono<D> toEntity(Class<D> entityType);
/**
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
*/
<D> Mono<D> toEntity(ParameterizedTypeReference<D> entityType);
/**
* Decode the field to a list of entities with the given type.
* @param elementType the type of elements in the list
* @return {@code Mono} with the list of decoded entities, possibly an
* empty list if the field {@link ResponseField#getValue() value} is
* {@code null} or empty
* @throws FieldAccessException if the target field is not
* {@link ResponseField#isValid() valid} or has any errors, including
* nested errors.
*/
<D> Mono<List<D>> toEntityList(Class<D> elementType);
/**
* Variant of {@link #toEntityList(Class)} with {@link ParameterizedTypeReference}.
*/
<D> Mono<List<D>> toEntityList(ParameterizedTypeReference<D> elementType);
}
/**
* Declares options to decode a field in each response of a subscription.
*/
interface RetrieveSubscriptionSpec {
/**
* Decode the field to an entity of the given type.
* @param entityType the type to convert to
* @return {@code Mono} with the decoded entity, possibly empty if the field
* {@link ResponseField#getValue() value} is {@code null}
* @throws FieldAccessException if the target field is not
* {@link ResponseField#isValid() valid} or has any errors, including
* nested errors.
*/
<D> Flux<D> toEntity(Class<D> entityType);
/**
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
*/
<D> Flux<D> toEntity(ParameterizedTypeReference<D> entityType);
/**
* Decode the field to a list of entities with the given type.
* @param elementType the type of elements in the list
* @return {@code Mono} with the list of decoded entities, possibly an
* empty list if the field {@link ResponseField#getValue() value} is
* {@code null} or empty
* @throws FieldAccessException if the target field is not
* {@link ResponseField#isValid() valid} or has any errors, including
* nested errors.
*/
<D> Flux<List<D>> toEntityList(Class<D> elementType);
/**
* Variant of {@link #toEntityList(Class)} with {@link ParameterizedTypeReference}.
*/
<D> Flux<List<D>> toEntityList(ParameterizedTypeReference<D> elementType);
}
}

View File

@@ -84,10 +84,6 @@ public interface ResponseField {
/**
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
* @param entityType the type to convert to
* @return the decoded entity, possibly {@code null} if the field
* {@link #getValue() value} is {@code null}
* @throws FieldAccessException if "this" field is not {@link #isValid() valid}
*/
@Nullable
<D> D toEntity(ParameterizedTypeReference<D> entityType);
@@ -102,9 +98,6 @@ public interface ResponseField {
/**
* Variant of {@link #toEntityList(Class)} with {@link ParameterizedTypeReference}.
* @param elementType the type of elements in the list
* @return the decoded list of entities, possibly empty
* @throws FieldAccessException if "this" field is not {@link #isValid() valid}
*/
<D> List<D> toEntityList(ParameterizedTypeReference<D> elementType);

View File

@@ -49,7 +49,6 @@ public class MapGraphQlResponse implements GraphQlResponse {
private final List<GraphQLError> errors;
@SuppressWarnings("unchecked")
protected MapGraphQlResponse(Map<String, Object> responseMap) {
Assert.notNull(responseMap, "'responseMap' is required");
this.responseMap = responseMap;

View File

@@ -40,21 +40,22 @@ public class GraphQlClientBuilderTests extends GraphQlClientTestSupport {
DocumentSource documentSource = name -> name.equals("name") ?
Mono.just(DOCUMENT) : Mono.error(new IllegalArgumentException());
setMockResponse("{}");
initDataResponse(DOCUMENT, "{}");
// Original
GraphQlClient.Builder<?> builder = graphQlClientBuilder().documentSource(documentSource);
GraphQlClient client = builder.build();
client.documentName("name").execute().block(TIMEOUT);
ClientGraphQlResponse response = client.documentName("name").execute().block(TIMEOUT);
GraphQlRequest request = request();
assertThat(request.getDocument()).isEqualTo(DOCUMENT);
assertThat(response).isNotNull();
assertThat(response.isValid()).isTrue();
// Mutate
client = client.mutate().build();
client.documentName("name").execute().block(TIMEOUT);
response = client.documentName("name").execute().block(TIMEOUT);
assertThat(request().getDocument()).isEqualTo(DOCUMENT);
assertThat(response).isNotNull();
assertThat(response.isValid()).isTrue();
}
}

View File

@@ -19,7 +19,6 @@ package org.springframework.graphql.client;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Consumer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -30,9 +29,11 @@ import org.mockito.ArgumentCaptor;
import reactor.core.publisher.Mono;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.support.MapGraphQlResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -71,25 +72,37 @@ public class GraphQlClientTestSupport {
}
protected void setMockResponse(String data) {
setMockResponse(builder -> serialize(data, builder));
protected void initDataResponse(String document, String responseData) {
initResponse(new GraphQlRequest(document), responseData);
}
protected void setMockResponse(GraphQLError... errors) {
setMockResponse(builder -> builder.errors(Arrays.asList(errors)));
protected void initErrorResponse(String document, GraphQLError... errors) {
initResponse(new GraphQlRequest(document), null, errors);
}
private void setMockResponse(Consumer<ExecutionResultImpl.Builder> consumer) {
protected void initResponse(String document, String responseData, GraphQLError... errors) {
initResponse(new GraphQlRequest(document), responseData, errors);
}
protected void initResponse(GraphQlRequest request, @Nullable String responseData, GraphQLError... errors) {
ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder();
consumer.accept(builder);
ExecutionResult result = builder.build();
GraphQlResponse response = MapGraphQlResponse.forResponse(result.toSpecification());
when(this.transport.execute(this.requestCaptor.capture())).thenReturn(Mono.just(response));
if (responseData != null) {
builder.data(decode(responseData));
}
if (!ObjectUtils.isEmpty(errors)) {
builder.errors(Arrays.asList(errors));
}
ExecutionResult executionResult = builder.build();
Map<String, Object> responseMap = executionResult.toSpecification();
when(this.transport.execute(eq(request)))
.thenReturn(Mono.just(MapGraphQlResponse.forResponse(responseMap)));
}
private void serialize(String data, ExecutionResultImpl.Builder builder) {
@SuppressWarnings("unchecked")
private <T> T decode(String data) {
try {
builder.data(OBJECT_MAPPER.readValue(data, Map.class));
return (T) OBJECT_MAPPER.readValue(data, Map.class);
}
catch (JsonProcessingException ex) {
throw new IllegalStateException(ex);

View File

@@ -15,10 +15,16 @@
*/
package org.springframework.graphql.client;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import graphql.execution.NonNullableValueCoercedAsNullException;
import graphql.execution.ResultPath;
import graphql.schema.GraphQLObjectType;
import graphql.validation.ValidationError;
import graphql.validation.ValidationErrorType;
import org.junit.jupiter.api.Test;
@@ -27,6 +33,7 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.GraphQlRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
@@ -37,111 +44,165 @@ import static org.assertj.core.api.Assertions.assertThat;
public class GraphQlClientTests extends GraphQlClientTestSupport {
@Test
void entity() {
String document = "{me {name}}";
setMockResponse("{\"me\": {\"name\":\"Luke Skywalker\"}}");
void retrieveEntity() {
MovieCharacter character = MovieCharacter.create("Luke Skywalker");
String document = "mockRequest1";
initDataResponse(document, "{\"me\": {\"name\":\"Luke Skywalker\"}}");
ClientGraphQlResponse response = execute(document);
assertThat(response.isValid()).isTrue();
MovieCharacter movieCharacter = graphQlClient().document(document)
.retrieve("me").toEntity(MovieCharacter.class)
.block(TIMEOUT);
ResponseField field = response.field("me");
assertThat(field.isValid()).isTrue();
assertThat(field.toEntity(MovieCharacter.class)).isEqualTo(character);
Map<String, MovieCharacter> map = response.toEntity(new ParameterizedTypeReference<Map<String, MovieCharacter>>() {});
assertThat(map).containsEntry("me", character);
assertThat(request().getDocument()).contains(document);
assertThat(movieCharacter).isEqualTo(MovieCharacter.create("Luke Skywalker"));
}
@Test
void entityList() {
void retrieveEntityList() {
String document = "{me {name, friends}}";
setMockResponse("{" +
String document = "mockRequest1";
initDataResponse(document, "{" +
" \"me\":{" +
" \"name\":\"Luke Skywalker\","
+ " \"friends\":[{\"name\":\"Han Solo\"}, {\"name\":\"Leia Organa\"}]" +
" }" +
"}");
MovieCharacter han = MovieCharacter.create("Han Solo");
MovieCharacter leia = MovieCharacter.create("Leia Organa");
ClientGraphQlResponse response = execute(document);
assertThat(response.isValid()).isTrue();
ResponseField field = response.field("me.friends");
assertThat(field.toEntityList(MovieCharacter.class)).containsExactly(han, leia);
assertThat(field.toEntityList(new ParameterizedTypeReference<MovieCharacter>() {})).containsExactly(han, leia);
assertThat(request().getDocument()).contains(document);
}
@Test
void operationNameAndVariables() {
String document = "query HeroNameAndFriends($episode: Episode) {" +
" hero(episode: $episode) {" +
" name"
+ " }" +
"}";
setMockResponse("{\"hero\": {\"name\":\"R2-D2\"}}");
ClientGraphQlResponse response = graphQlClient().document(document)
.operationName("HeroNameAndFriends")
.variable("episode", "JEDI")
.variable("foo", "bar")
.variable("keyOnly", null)
.execute()
List<MovieCharacter> movieCharacters = graphQlClient().document(document)
.retrieve("me.friends")
.toEntityList(MovieCharacter.class)
.block(TIMEOUT);
assertThat(response).isNotNull();
assertThat(response.isValid()).isTrue();
assertThat(response.field("hero").toEntity(MovieCharacter.class)).isEqualTo(MovieCharacter.create("R2-D2"));
GraphQlRequest request = request();
assertThat(request.getDocument()).contains(document);
assertThat(request.getOperationName()).isEqualTo("HeroNameAndFriends");
assertThat(request.getVariables()).hasSize(3);
assertThat(request.getVariables()).containsEntry("episode", "JEDI");
assertThat(request.getVariables()).containsEntry("foo", "bar");
assertThat(request.getVariables()).containsEntry("keyOnly", null);
assertThat(movieCharacters).containsExactly(
MovieCharacter.create("Han Solo"), MovieCharacter.create("Leia Organa"));
}
@Test
void requestFailureBeforeExecution() {
void retrieveAndDecodeDataMap() {
String document = "{invalid";
setMockResponse(new ValidationError(ValidationErrorType.InvalidSyntax));
String document = "mockRequest1";
initDataResponse(document, "{\"me\": {\"name\":\"Luke Skywalker\"}}");
ClientGraphQlResponse response = execute(document);
Map<String, MovieCharacter> map = graphQlClient().document(document)
.retrieve("").toEntity(new ParameterizedTypeReference<Map<String, MovieCharacter>>() {})
.block(TIMEOUT);
assertThat(map).containsEntry("me", MovieCharacter.create("Luke Skywalker"));
}
@Test
void retrieveWithOperationNameAndVariables() {
String document = "mockRequest1";
String operationName = "HeroNameAndFriends";
Map<String, Object> vars = new HashMap<>();
vars.put("episode", "JEDI");
vars.put("foo", "bar");
vars.put("keyOnly", null);
GraphQlRequest request = new GraphQlRequest("mockRequest1", "HeroNameAndFriends", vars);
initResponse(request, "{\"hero\": {\"name\":\"R2-D2\"}}");
MovieCharacter character = graphQlClient().document(document)
.operationName(operationName)
.variables(vars)
.variable("keyOnly", null)
.retrieve("hero")
.toEntity(MovieCharacter.class)
.block(TIMEOUT);
assertThat(character).isEqualTo(MovieCharacter.create("R2-D2"));
}
@Test
void retrieveInvalidResponse() {
String document = "errorsOnlyResponse";
initErrorResponse(document, new ValidationError(ValidationErrorType.InvalidSyntax));
testRetrieveFieldAccessException(document, "me");
document = "nullDataResponse";
GraphQLObjectType type = GraphQLObjectType.newObject().name("n").build();
initResponse(document, "null", new NonNullableValueCoercedAsNullException("f", new ArrayList<>(), type));
testRetrieveFieldAccessException(document, "me");
}
@Test
void retrievePartialResponse() {
String document = "fieldErrorResponse";
initResponse(document, "{\"me\": {\"name\":null}}", errorForPath("/me/name"));
testRetrieveFieldAccessException(document, "me");
testRetrieveFieldAccessException(document, "me.name");
}
private void testRetrieveFieldAccessException(String document, String path) {
assertThatThrownBy(() ->
graphQlClient().document(document)
.retrieve(path).toEntity(MovieCharacter.class)
.block(TIMEOUT))
.isInstanceOf(FieldAccessException.class);
}
@Test
void executeInvalidResponse() {
String document = "errorsOnlyResponse";
initErrorResponse(document, new ValidationError(ValidationErrorType.InvalidSyntax));
testExecuteFailedResponse(document);
document = "nullDataResponse";
GraphQLObjectType type = GraphQLObjectType.newObject().name("n").build();
initResponse(document, "null", new NonNullableValueCoercedAsNullException("f", new ArrayList<>(), type));
testExecuteFailedResponse(document);
}
private void testExecuteFailedResponse(String document) {
ClientGraphQlResponse response =
graphQlClient().document(document).execute().block(TIMEOUT);
assertThat(response).isNotNull();
assertThat(response.isValid()).isFalse();
assertThat(response.field("me").isValid()).isFalse();
assertThatThrownBy(() -> response.field("me").toEntity(MovieCharacter.class))
.isInstanceOf(FieldAccessException.class);
}
@Test
void errors() {
void executePartialResponse() {
String document = "{me {name, friends}}";
setMockResponse(
GraphqlErrorBuilder.newError().message("some error").build(),
GraphqlErrorBuilder.newError().message("some other error").build());
String document = "fieldErrorResponse";
initResponse(document, "{\"me\": {\"name\":null}}", errorForPath("/me/name"));
testRetrieveFieldAccessException(document, "me.name");
ClientGraphQlResponse response = execute(document);
assertThat(response.isValid()).isFalse();
ClientGraphQlResponse response =
graphQlClient().document(document).execute().block(TIMEOUT);
assertThat(response.getErrors())
.extracting(GraphQLError::getMessage)
.containsExactly("some error", "some other error");
assertThat(response).isNotNull();
assertThat(response.isValid())
.as("Partial response with field errors should be considered valid")
.isTrue();
ResponseField field = response.field("me");
assertThat(field.isValid()).isTrue();
assertThat(field.toEntity(MovieCharacter.class))
.as("Decoding with nested field error should not be precluded")
.isNotNull();
ResponseField nameField = response.field("me.name");
assertThat(nameField.isValid()).isFalse();
assertThatThrownBy(() -> nameField.toEntity(String.class))
.as("Decoding field null with direct field error should be rejected")
.isInstanceOf(FieldAccessException.class);
}
private ClientGraphQlResponse execute(String document) {
ClientGraphQlResponse response = graphQlClient().document(document).execute().block(TIMEOUT);
assertThat(response).isNotNull();
return response;
private GraphQLError errorForPath(String errorPath) {
return GraphqlErrorBuilder.newError()
.message("Test error")
.path(ResultPath.parse(errorPath).toList()).build();
}
}