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:
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user