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:
@@ -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() + "'" +
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -63,6 +63,11 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public GraphQlRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseField field(String path) {
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user