Refine null handling in GraphQlClient for retrieve

If a field is null but without errors, i.e. declared optional in the
schema, it is more natural for toEntity to complete empty instead of
raising a FieldAccessException.

This aligns better with the execute method where handling the field
directly allows treating a null but valid field as optional.

See gh-10
This commit is contained in:
rstoyanchev
2022-03-17 15:36:01 +00:00
parent b2b1e2d506
commit 3c30376cba
4 changed files with 51 additions and 28 deletions

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.graphql.client;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -197,13 +198,17 @@ final class DefaultGraphQlClient implements GraphQlClient {
this.path = path;
}
protected ResponseField getField(ClientGraphQlResponse response) {
/**
* Return the field if valid, possibly {@code null}.
* @throws FieldAccessException if the response or field is not valid
*/
@Nullable
protected ResponseField getValidField(ClientGraphQlResponse response) {
ResponseField field = response.field(this.path);
if (!field.hasValue() || !field.getErrors().isEmpty()) {
GraphQlRequest request = response.getRequest();
throw new FieldAccessException(request, response, field);
if (!response.isValid() || field.getError() != null) {
throw new FieldAccessException(response.getRequest(), response, field);
}
return field;
return (field.hasValue() ? field : null);
}
}
@@ -220,22 +225,28 @@ final class DefaultGraphQlClient implements GraphQlClient {
@Override
public <D> Mono<D> toEntity(Class<D> entityType) {
return this.responseMono.map(this::getField).map(field -> field.toEntity(entityType));
return this.responseMono.mapNotNull(this::getValidField).map(field -> field.toEntity(entityType));
}
@Override
public <D> Mono<D> toEntity(ParameterizedTypeReference<D> entityType) {
return this.responseMono.map(this::getField).map(field -> field.toEntity(entityType));
return this.responseMono.mapNotNull(this::getValidField).map(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));
return this.responseMono.map(response -> {
ResponseField field = getValidField(response);
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
});
}
@Override
public <D> Mono<List<D>> toEntityList(ParameterizedTypeReference<D> elementType) {
return this.responseMono.map(this::getField).map(field -> field.toEntityList(elementType));
return this.responseMono.map(response -> {
ResponseField field = getValidField(response);
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
});
}
}
@@ -252,22 +263,28 @@ final class DefaultGraphQlClient implements GraphQlClient {
@Override
public <D> Flux<D> toEntity(Class<D> entityType) {
return this.responseFlux.map(this::getField).map(field -> field.toEntity(entityType));
return this.responseFlux.mapNotNull(this::getValidField).map(field -> field.toEntity(entityType));
}
@Override
public <D> Flux<D> toEntity(ParameterizedTypeReference<D> entityType) {
return this.responseFlux.map(this::getField).map(field -> field.toEntity(entityType));
return this.responseFlux.mapNotNull(this::getValidField).map(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));
return this.responseFlux.map(response -> {
ResponseField field = getValidField(response);
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
});
}
@Override
public <D> Flux<List<D>> toEntityList(ParameterizedTypeReference<D> elementType) {
return this.responseFlux.map(this::getField).map(field -> field.toEntityList(elementType));
return this.responseFlux.map(response -> {
ResponseField field = getValidField(response);
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
});
}
}

View File

@@ -155,8 +155,8 @@ public interface GraphQlClient {
/**
* 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:
* @return a {@code Flux} with responses that provide further options for
* decoding of each response. The {@code Flux} may terminate as follows:
* <ul>
* <li>Completes if the subscription completes before the connection is closed.
* <li>{@link SubscriptionErrorException} if the subscription ends with an error.
@@ -180,9 +180,10 @@ public interface GraphQlClient {
/**
* Decode the field to an entity of the given type.
* @param entityType the type to convert to
* @return {@code Mono} with the decoded entity, or a
* {@link FieldAccessException} if the target field is not present or
* has no value, checked via {@link ResponseField#hasValue()}.
* @return {@code Mono} that provides the decoded entity, or completes
* empty when the field is {@code null} but without errors, or ends with
* a {@link FieldAccessException} if the target field is not present or
* has no value.
*/
<D> Mono<D> toEntity(Class<D> entityType);
@@ -194,9 +195,9 @@ public interface GraphQlClient {
/**
* Decode the field to a list of entities with the given type.
* @param elementType the type of elements in the list
* @return {@code Mono} with a list of decoded entities, possibly empty, or
* a {@link FieldAccessException} if the target field is not present or
* has no value, checked via {@link ResponseField#hasValue()}; the stream
* @return {@code Mono} with a list of decoded entities, possibly an
* empty list, or ends with {@link FieldAccessException} if the target
* field is not present or has no value.
*/
<D> Mono<List<D>> toEntityList(Class<D> elementType);
@@ -216,9 +217,11 @@ public interface GraphQlClient {
/**
* Decode the field to an entity of the given type.
* @param entityType the type to convert to
* @return {@code Mono} with the decoded entity, or a
* @return decoded entities, one for each response, except responses
* in which the field is {@code null} but without errors, or ending with
* {@link FieldAccessException} if the target field is not present or
* has no value, checked via {@link ResponseField#hasValue()}.
* has no value in a given response; the stream may also end with a
* {@link GraphQlTransportException}.
*/
<D> Flux<D> toEntity(Class<D> entityType);
@@ -230,10 +233,11 @@ public interface GraphQlClient {
/**
* Decode the field to a list of entities with the given type.
* @param elementType the type of elements in the list
* @return lists of decoded entities, possibly empty, or a
* @return lists of decoded entities, one for each response, except responses
* in which the field is {@code null} but without errors, or ending with
* {@link FieldAccessException} if the target field is not present or
* has no value, checked via {@link ResponseField#hasValue()}; the stream
* may also end with a range of {@link GraphQlTransportException} types.
* has no value in a given response; the stream may also end with a
* {@link GraphQlTransportException}.
*/
<D> Flux<List<D>> toEntityList(Class<D> elementType);

View File

@@ -34,7 +34,7 @@ import org.springframework.lang.Nullable;
public interface ResponseField {
/**
* Whether the field is valid and has a value.
* Whether the field has a value.
* <ul>
* <li>{@code "true"} means the field is not {@code null} in which case there
* is no field {@link #getError() error}. The field may still be partial and

View File

@@ -133,7 +133,9 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
String document = "fieldErrorResponse";
initResponse(document, "{\"me\": {\"name\":null}}", errorForPath("/me/name"));
testRetrieveFieldAccessException(document, "me");
MovieCharacter character = graphQlClient().document(document).retrieve("me").toEntity(MovieCharacter.class).block();
assertThat(character).isNotNull().extracting(MovieCharacter::getName).isNull();
testRetrieveFieldAccessException(document, "me.name");
}