Introduce ClientGraphQlResponse

Extends GraphQlResponse with a subtype that exposes further options to
handle data and errors in the response from a client perspective.

See gh-10
This commit is contained in:
rstoyanchev
2022-03-11 15:12:09 +00:00
parent 15238072bb
commit dcb752dde4
10 changed files with 479 additions and 222 deletions

View File

@@ -170,7 +170,7 @@ final class DefaultWebSocketGraphQlTester extends AbstractDelegatingGraphQlTeste
.operationName(request.getOperationName())
.variables(request.getVariables())
.execute()
.map(GraphQlClient.Response::andReturn);
.cast(GraphQlResponse.class);
}
@Override
@@ -179,7 +179,8 @@ final class DefaultWebSocketGraphQlTester extends AbstractDelegatingGraphQlTeste
.document(request.getDocument())
.operationName(request.getOperationName())
.variables(request.getVariables())
.executeSubscription().map(GraphQlClient.Response::andReturn);
.executeSubscription()
.cast(GraphQlResponse.class);
}
};
}

View File

@@ -0,0 +1,56 @@
/*
* 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 org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.GraphQlResponse;
/**
* {@link GraphQlResponse} for client use with further options to navigate and
* handle the selection set in the response.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public interface ClientGraphQlResponse extends GraphQlResponse {
/**
* Navigate to the given path under the "data" key of the response map and
* return a representation with further options to decode the field value,
* or to check whether it's valid, and so on.
* @param path relative to the "data" key.
* @return a representation for the field at the given path; this
*/
ResponseField field(String path);
/**
* Decode the full response map to the given target type.
* @param type the target class
* @return the decoded value
*/
<D> D toEntity(Class<D> type);
/**
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
* @param type the target type
* @return the decoded value
*/
<D> D toEntity(ParameterizedTypeReference<D> type);
}

View File

@@ -0,0 +1,252 @@
/*
* 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.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import com.jayway.jsonpath.TypeRef;
import graphql.GraphQLError;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.support.MapGraphQlResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* Default implementation of {@link ClientGraphQlResponse}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
class DefaultClientGraphQlResponse extends MapGraphQlResponse implements ClientGraphQlResponse {
private final DocumentContext jsonPathDoc;
DefaultClientGraphQlResponse(GraphQlResponse response, Configuration jsonPathConfig) {
super(response.toMap());
this.jsonPathDoc = JsonPath.parse(response.toMap(), jsonPathConfig);
}
@Override
public <D> D toEntity(Class<D> type) {
assertValidResponse();
return field("").toEntity(type);
}
@Override
public <D> D toEntity(ParameterizedTypeReference<D> type) {
assertValidResponse();
return field("").toEntity(type);
}
@Override
public ResponseField field(String path) {
path = "$.data" + (StringUtils.hasText(path) ? "." + path : "");
return new DefaultField(path, this.jsonPathDoc, getErrors());
}
private void assertValidResponse() {
if (!isValid()) {
throw new IllegalStateException("Path not present exception");
}
}
/**
* Default implementation of {@link ResponseField}.
*/
private static class DefaultField implements ResponseField {
private final String path;
private final DocumentContext jsonPathDoc;
private final List<GraphQLError> errorsAt;
private final List<GraphQLError> errorsBelow;
private final boolean exists;
@Nullable
private final Object value;
public DefaultField(String path, DocumentContext jsonPathDoc, List<GraphQLError> errors) {
Assert.notNull(path, "'path' is required");
this.path = path;
this.jsonPathDoc = jsonPathDoc;
List<GraphQLError> errorsAt = null;
List<GraphQLError> errorsBelow = null;
for (GraphQLError error : errors) {
String errorPath = toJsonPath(error);
if (errorPath == null) {
continue;
}
if (errorPath.equals(path)) {
errorsAt = (errorsAt != null ? errorsAt : new ArrayList<>());
errorsAt.add(error);
}
if (errorPath.startsWith(path)) {
errorsBelow = (errorsBelow != null ? errorsBelow : new ArrayList<>());
errorsBelow.add(error);
}
}
this.errorsAt = (errorsAt != null ? errorsAt : Collections.emptyList());
this.errorsBelow = (errorsBelow != null ? errorsBelow : Collections.emptyList());
boolean exists = true;
Object value = null;
try {
value = jsonPathDoc.read(this.path);
}
catch (PathNotFoundException ex) {
exists = false;
}
this.exists = exists;
this.value = value;
}
@Nullable
private String toJsonPath(GraphQLError error) {
if (CollectionUtils.isEmpty(error.getPath())) {
return null;
}
List<Object> segments = error.getPath();
StringBuilder sb = new StringBuilder((String) segments.get(0));
for (int i = 1; i < segments.size(); i++) {
Object segment = segments.get(i);
if (segment instanceof Integer) {
sb.append("[").append(segment).append("]");
}
else {
sb.append(".").append(segment);
}
}
return sb.toString();
}
@Override
public String getPath() {
return this.path;
}
@Override
public boolean isValid() {
return (this.exists && (this.value != null || (this.errorsAt.isEmpty() && this.errorsBelow.isEmpty())));
}
@SuppressWarnings("unchecked")
@Override
public <T> T getValue() {
return (T) this.value;
}
@Override
public List<GraphQLError> getErrorsAt() {
return this.errorsAt;
}
@Override
public List<GraphQLError> getErrorsBelow() {
return this.errorsBelow;
}
@Override
public <D> D toEntity(Class<D> entityType) {
assertValidField();
return this.jsonPathDoc.read(this.path, new TypeRefAdapter<>(entityType));
}
@Override
public <D> D toEntity(ParameterizedTypeReference<D> entityType) {
assertValidField();
return this.jsonPathDoc.read(this.path, new TypeRefAdapter<>(entityType));
}
@Override
public <D> List<D> toEntityList(Class<D> elementType) {
assertValidField();
return this.jsonPathDoc.read(this.path, new TypeRefAdapter<>(List.class, elementType));
}
@Override
public <D> List<D> toEntityList(ParameterizedTypeReference<D> elementType) {
assertValidField();
return this.jsonPathDoc.read(this.path, new TypeRefAdapter<>(List.class, elementType));
}
private void assertValidField() {
if (!isValid()) {
throw (CollectionUtils.isEmpty(this.errorsAt) ?
new IllegalStateException("Path not present exception") :
new IllegalStateException("Field error exception"));
}
}
}
/**
* Adapt JSONPath {@link TypeRef} to {@link ParameterizedTypeReference}.
*/
private static final class TypeRefAdapter<T> extends TypeRef<T> {
private final Type type;
TypeRefAdapter(Class<T> clazz) {
this.type = clazz;
}
TypeRefAdapter(ParameterizedTypeReference<T> typeReference) {
this.type = typeReference.getType();
}
TypeRefAdapter(Class<?> clazz, Class<?> generic) {
this.type = ResolvableType.forClassWithGenerics(clazz, generic).getType();
}
TypeRefAdapter(Class<?> clazz, ParameterizedTypeReference<?> generic) {
this.type = ResolvableType.forClassWithGenerics(clazz, ResolvableType.forType(generic)).getType();
}
@Override
public Type getType() {
return this.type;
}
}
}

View File

@@ -15,28 +15,18 @@
*/
package org.springframework.graphql.client;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.TypeRef;
import graphql.GraphQLError;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Default, final {@link GraphQlClient} implementation for use with any transport.
@@ -152,20 +142,20 @@ final class DefaultGraphQlClient implements GraphQlClient {
}
@Override
public Mono<Response> execute() {
return getRequestMono()
.flatMap(this.transport::execute)
.map(payload -> new DefaultResponse(payload, this.jsonPathConfig));
public Mono<ClientGraphQlResponse> execute() {
return initRequest().flatMap(request ->
this.transport.execute(request)
.map(response -> new DefaultClientGraphQlResponse(response, this.jsonPathConfig)));
}
@Override
public Flux<Response> executeSubscription() {
return getRequestMono()
.flatMapMany(this.transport::executeSubscription)
.map(payload -> new DefaultResponse(payload, this.jsonPathConfig));
public Flux<ClientGraphQlResponse> executeSubscription() {
return initRequest().flatMapMany(request ->
this.transport.executeSubscription(request)
.map(response -> new DefaultClientGraphQlResponse(response, this.jsonPathConfig)));
}
private Mono<GraphQlRequest> getRequestMono() {
private Mono<GraphQlRequest> initRequest() {
return this.documentMono.map(document ->
new GraphQlRequest(document, this.operationName, this.variables));
}
@@ -173,93 +163,4 @@ final class DefaultGraphQlClient implements GraphQlClient {
}
/**
* Default {@link GraphQlClient.Response} implementation.
*/
private static class DefaultResponse implements Response {
private final GraphQlResponse response;
private final DocumentContext jsonPathDoc;
private final List<GraphQLError> errors;
private DefaultResponse(GraphQlResponse response, Configuration jsonPathConfig) {
this.response = response;
this.jsonPathDoc = JsonPath.parse(response.toMap(), jsonPathConfig);
this.errors = response.getErrors();
}
@Override
public <D> D toEntity(String path, Class<D> entityType) {
return this.jsonPathDoc.read(initJsonPath(path), new TypeRefAdapter<>(entityType));
}
@Override
public <D> D toEntity(String path, ParameterizedTypeReference<D> entityType) {
return this.jsonPathDoc.read(initJsonPath(path), new TypeRefAdapter<>(entityType));
}
@Override
public <D> List<D> toEntityList(String path, Class<D> elementType) {
return this.jsonPathDoc.read(initJsonPath(path), new TypeRefAdapter<>(List.class, elementType));
}
@Override
public <D> List<D> toEntityList(String path, ParameterizedTypeReference<D> elementType) {
return this.jsonPathDoc.read(initJsonPath(path), new TypeRefAdapter<>(List.class, elementType));
}
private static JsonPath initJsonPath(String path) {
if (!StringUtils.hasText(path)) {
path = "$.data";
}
else if (!path.startsWith("$") && !path.startsWith("data.")) {
path = "$.data." + path;
}
return JsonPath.compile(path);
}
@Override
public List<GraphQLError> errors() {
return this.errors;
}
@Override
public GraphQlResponse andReturn() {
return this.response;
}
}
/**
* Adapt JSONPath {@link TypeRef} to {@link ParameterizedTypeReference}.
*/
private static final class TypeRefAdapter<T> extends TypeRef<T> {
private final Type type;
TypeRefAdapter(Class<T> clazz) {
this.type = clazz;
}
TypeRefAdapter(ParameterizedTypeReference<T> typeReference) {
this.type = typeReference.getType();
}
TypeRefAdapter(Class<?> clazz, Class<?> generic) {
this.type = ResolvableType.forClassWithGenerics(clazz, generic).getType();
}
TypeRefAdapter(Class<?> clazz, ParameterizedTypeReference<?> generic) {
this.type = ResolvableType.forClassWithGenerics(clazz, ResolvableType.forType(generic)).getType();
}
@Override
public Type getType() {
return this.type;
}
}
}

View File

@@ -15,15 +15,11 @@
*/
package org.springframework.graphql.client;
import java.util.List;
import java.util.Map;
import graphql.GraphQLError;
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;
@@ -135,15 +131,15 @@ public interface GraphQlClient {
/**
* Execute as a request with a single response such as a "query" or
* "mutation" operation.
* @return a {@code Mono} with a {@code ResponseSpec} for further
* @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.
*/
Mono<Response> execute();
Mono<ClientGraphQlResponse> execute();
/**
* Execute a "subscription" request with a stream of responses.
* @return a {@code Flux} with a {@code ResponseSpec} for further
* @return a {@code Flux} with a {@code ClientGraphQlResponse} for further
* decoding of the response. The {@code Flux} may terminate as follows:
* <ul>
* <li>Completes if the subscription completes before the connection is closed.
@@ -155,72 +151,7 @@ public interface GraphQlClient {
* <p>The {@code Flux} may be cancelled to notify the server to end the
* subscription stream.
*/
Flux<Response> executeSubscription();
}
/**
* Declare options to decode a response.
*/
interface Response {
/**
* Switch to the given the "data" path of the GraphQL response and
* convert the data to the target type. The path can be an operation
* root type name, e.g. "book", or a nested path such as "book.name",
* or any <a href="https://github.com/jayway/JsonPath">JsonPath</a>
* relative to the "data" key of the response.
* @param path a JSON path to the data of interest
* @param entityType the type to convert to
* @param <D> the target entity type
* @return the entity resulting from the conversion
*/
<D> D toEntity(String path, Class<D> entityType);
/**
* Variant of {@link #toEntity(String, Class)} for entity classes with
* generic types.
* @param path a JSON path to the data of interest
* @param entityType the type to convert to
* @param <D> the target entity type
* @return the entity resulting from the conversion
*/
<D> D toEntity(String path, ParameterizedTypeReference<D> entityType);
/**
* Switch to the given the "data" path of the GraphQL response and
* convert the data to a List with the given element type.
* The path can be an operation root type name, e.g. "book", or a
* nested path such as "book.name", or any
* <a href="https://github.com/jayway/JsonPath">JsonPath</a>
* relative to the "data" key of the response.
* @param path a JSON path to the data of interest
* @param elementType the type of element to convert to
* @param <D> the target entity type
* @return the list of entities resulting from the conversion
*/
<D> List<D> toEntityList(String path, Class<D> elementType);
/**
* Variant of {@link #toEntityList(String, Class)} for entity classes
* with generic types.
* @param path a JSON path to the data of interest
* @param elementType the type to convert to
* @param <D> the target entity type
* @return the list of entities resulting from the conversion
*/
<D> List<D> toEntityList(String path, ParameterizedTypeReference<D> elementType);
/**
* Return the errors from the response or an empty list.
*/
List<GraphQLError> errors();
/**
* Return the underlying {@link GraphQlResponse}.
*/
GraphQlResponse andReturn();
Flux<ClientGraphQlResponse> executeSubscription();
}

View File

@@ -0,0 +1,100 @@
/*
* 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.List;
import graphql.GraphQLError;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.lang.Nullable;
/**
* Representation for a field in a GraphQL response, with options to examine its
* value and errors, and to decode it.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public interface ResponseField {
/**
* Whether the field is valid. A field is invalid if:
* <ul>
* <li>the path doesn't exist
* <li>it is {@code null} AND has a field error
* </ul>
* <p>A field that is not {@code null} is valid but may still be partial
* with some fields below it set to {@code null} due to field errors.
* A valid field may be {@code null} if the schema allows it, but in that
* case it will not have any field errors.
*/
boolean isValid();
/**
* Return the path for the field under the "data" key in the response map.
*/
String getPath();
/**
* Return the field value without any decoding.
* @param <T> the expected value type, e.g. Map, List, or a scalar type.
* @return the value
*/
@Nullable
<T> T getValue();
/**
* Return errors with paths matching that of the field.
*/
List<GraphQLError> getErrorsAt();
/**
* Return errors with paths below that of the field.
*/
List<GraphQLError> getErrorsBelow();
/**
* Decode the field to an entity of the given type.
* @param entityType the type to convert to
* @return the entity instance
*/
<D> D toEntity(Class<D> entityType);
/**
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
* @param entityType the type to convert to
* @return the entity instance
*/
<D> 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 the list of entities
*/
<D> List<D> toEntityList(Class<D> elementType);
/**
* Variant of {@link #toEntityList(Class)} with {@link ParameterizedTypeReference}.
* @param elementType the type of elements in the list
* @return the list of entities
*/
<D> List<D> toEntityList(ParameterizedTypeReference<D> elementType);
}

View File

@@ -496,15 +496,15 @@ final class WebSocketGraphQlTransport implements GraphQlTransport {
return;
}
List<Map<String, Object>> payload = message.getPayload();
List<Map<String, Object>> errorList = message.getPayload();
Sinks.EmitResult emitResult;
if (sink != null) {
GraphQlResponse response = MapGraphQlResponse.forErrorsOnly(payload);
GraphQlResponse response = MapGraphQlResponse.forErrorsOnly(errorList);
emitResult = sink.tryEmitValue(response);
}
else {
List<GraphQLError> graphQLErrors = MapGraphQlError.from(payload);
List<GraphQLError> graphQLErrors = MapGraphQlError.from(errorList);
Exception ex = new SubscriptionErrorException(graphQLErrors);
emitResult = streamingSink.tryEmitError(ex);
}

View File

@@ -32,24 +32,24 @@ import org.springframework.util.Assert;
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public final class MapGraphQlResponse implements GraphQlResponse {
public class MapGraphQlResponse implements GraphQlResponse {
private final Map<String, Object> resultMap;
private final Map<String, Object> responseMap;
private final List<GraphQLError> errors;
@SuppressWarnings("unchecked")
private MapGraphQlResponse(Map<String, Object> resultMap) {
Assert.notNull(resultMap, "'resultMap' is required");
this.resultMap = resultMap;
this.errors = MapGraphQlError.from((List<Map<String, Object>>) resultMap.get("errors"));
protected MapGraphQlResponse(Map<String, Object> responseMap) {
Assert.notNull(responseMap, "'responseMap' is required");
this.responseMap = responseMap;
this.errors = MapGraphQlError.from((List<Map<String, Object>>) responseMap.get("errors"));
}
@Override
public boolean isValid() {
return (this.resultMap.containsKey("data") && this.resultMap.get("data") != null);
return (this.responseMap.containsKey("data") && this.responseMap.get("data") != null);
}
@Override
@@ -60,34 +60,34 @@ public final class MapGraphQlResponse implements GraphQlResponse {
@SuppressWarnings("unchecked")
@Override
public <T> T getData() {
return (T) this.resultMap.get("data");
return (T) this.responseMap.get("data");
}
@SuppressWarnings("unchecked")
@Override
public Map<Object, Object> getExtensions() {
return (Map<Object, Object>) this.resultMap.getOrDefault("extensions", Collections.emptyMap());
return (Map<Object, Object>) this.responseMap.getOrDefault("extensions", Collections.emptyMap());
}
@Override
public Map<String, Object> toMap() {
return this.resultMap;
return this.responseMap;
}
@Override
public boolean equals(Object other) {
return (other instanceof MapGraphQlResponse &&
this.resultMap.equals(((MapGraphQlResponse) other).resultMap));
this.responseMap.equals(((MapGraphQlResponse) other).responseMap));
}
@Override
public int hashCode() {
return this.resultMap.hashCode();
return this.responseMap.hashCode();
}
@Override
public String toString() {
return this.resultMap.toString();
return this.responseMap.toString();
}

View File

@@ -15,11 +15,12 @@
*/
package org.springframework.graphql.client;
import java.util.List;
import java.util.Map;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import graphql.validation.ValidationError;
import graphql.validation.ValidationErrorType;
import org.junit.jupiter.api.Test;
import org.springframework.core.ParameterizedTypeReference;
@@ -40,14 +41,17 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
String document = "{me {name}}";
setMockResponse("{\"me\": {\"name\":\"Luke Skywalker\"}}");
GraphQlClient.Response response = execute(document);
MovieCharacter character = MovieCharacter.create("Luke Skywalker");
MovieCharacter luke = MovieCharacter.create("Luke Skywalker");
assertThat(response.toEntity("me", MovieCharacter.class)).isEqualTo(luke);
ClientGraphQlResponse response = execute(document);
assertThat(response.isValid()).isTrue();
Map<String, MovieCharacter> map = response.toEntity("", new ParameterizedTypeReference<Map<String, MovieCharacter>>() {});
assertThat(map).containsEntry("me", luke);
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);
}
@@ -58,20 +62,19 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
setMockResponse("{" +
" \"me\":{" +
" \"name\":\"Luke Skywalker\","
+ " \"friends\":[{\"name\":\"Han Solo\"}, {\"name\":\"Leia Organa\"}]" +
+ " \"friends\":[{\"name\":\"Han Solo\"}, {\"name\":\"Leia Organa\"}]" +
" }" +
"}");
GraphQlClient.Response response = execute(document);
MovieCharacter han = MovieCharacter.create("Han Solo");
MovieCharacter leia = MovieCharacter.create("Leia Organa");
List<MovieCharacter> characters = response.toEntityList("me.friends", MovieCharacter.class);
assertThat(characters).containsExactly(han, leia);
ClientGraphQlResponse response = execute(document);
assertThat(response.isValid()).isTrue();
characters = response.toEntityList("me.friends", new ParameterizedTypeReference<MovieCharacter>() {});
assertThat(characters).containsExactly(han, leia);
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);
}
@@ -86,7 +89,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
"}";
setMockResponse("{\"hero\": {\"name\":\"R2-D2\"}}");
GraphQlClient.Response response = graphQlClient().document(document)
ClientGraphQlResponse response = graphQlClient().document(document)
.operationName("HeroNameAndFriends")
.variable("episode", "JEDI")
.variable("foo", "bar")
@@ -95,9 +98,8 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
.block(TIMEOUT);
assertThat(response).isNotNull();
MovieCharacter character = response.toEntity("hero", MovieCharacter.class);
assertThat(character).isEqualTo(MovieCharacter.create("R2-D2"));
assertThat(response.isValid()).isTrue();
assertThat(response.field("hero").toEntity(MovieCharacter.class)).isEqualTo(MovieCharacter.create("R2-D2"));
GraphQlRequest request = request();
assertThat(request.getDocument()).contains(document);
@@ -108,6 +110,18 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
assertThat(request.getVariables()).containsEntry("keyOnly", null);
}
@Test
void requestFailureBeforeExecution() {
String document = "{invalid";
setMockResponse(new ValidationError(ValidationErrorType.InvalidSyntax));
ClientGraphQlResponse response = execute(document);
assertThat(response.isValid()).isFalse();
assertThat(response.field("me").isValid()).isFalse();
}
@Test
void errors() {
@@ -116,14 +130,16 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
GraphqlErrorBuilder.newError().message("some error").build(),
GraphqlErrorBuilder.newError().message("some other error").build());
GraphQlClient.Response response = execute(document);
ClientGraphQlResponse response = execute(document);
assertThat(response.isValid()).isFalse();
assertThat(response.errors()).extracting(GraphQLError::getMessage)
assertThat(response.getErrors())
.extracting(GraphQLError::getMessage)
.containsExactly("some error", "some other error");
}
private GraphQlClient.Response execute(String document) {
GraphQlClient.Response response = graphQlClient().document(document).execute().block(TIMEOUT);
private ClientGraphQlResponse execute(String document) {
ClientGraphQlResponse response = graphQlClient().document(document).execute().block(TIMEOUT);
assertThat(response).isNotNull();
return response;
}

View File

@@ -197,13 +197,13 @@ public class WebGraphQlClientBuilderTests {
.build());
WebGraphQlClient client = builder.build();
GraphQlClient.Response response = client.document(document).execute().block(TIMEOUT);
ClientGraphQlResponse response = client.document(document).execute().block(TIMEOUT);
testDecoder.resetLastValue();
assertThat(testDecoder.getLastValue()).isNull();
assertThat(response).isNotNull();
assertThat(response.toEntity("me", MovieCharacter.class).getName()).isEqualTo("Luke Skywalker");
assertThat(response.field("me").toEntity(MovieCharacter.class).getName()).isEqualTo("Luke Skywalker");
assertThat(testDecoder.getLastValue()).isEqualTo(character);
}