Replace isValid with hasValue in ResponseField
hasValue is more meaningful because it means valid and with some value that could be decoded, which in turn allows removing Nullable from toEntity/List. Or if there is no value, getError() can be used to differentiate between a failed field vs a null field declared optional. See gh-10
This commit is contained in:
@@ -20,7 +20,6 @@ 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;
|
||||
|
||||
/**
|
||||
* {@link GraphQlResponse} for client use, with further options to handle the
|
||||
@@ -49,27 +48,25 @@ public interface ClientGraphQlResponse extends GraphQlResponse {
|
||||
* </pre>
|
||||
* @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, has a value, or field errors
|
||||
* decode its value; use {@link ResponseField#hasValue()} to check if the
|
||||
* field actually exists and has a value.
|
||||
*/
|
||||
ResponseField field(String path);
|
||||
|
||||
/**
|
||||
* Decode the full response map to the given target type.
|
||||
* @param type the target class
|
||||
* @return the decoded value, or {@code null} if the "data" is {@code null}
|
||||
* @return the decoded value, or never {@code null}
|
||||
* @throws FieldAccessException if the response is not {@link #isValid() valid}
|
||||
*/
|
||||
@Nullable
|
||||
<D> D toEntity(Class<D> type);
|
||||
|
||||
/**
|
||||
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
|
||||
* @param type the target type
|
||||
* @return the decoded value, or {@code null} if the "data" is {@code null}
|
||||
* @return the decoded value, or never {@code null}
|
||||
* @throws FieldAccessException if the response is not {@link #isValid() valid}
|
||||
*/
|
||||
@Nullable
|
||||
<D> D toEntity(ParameterizedTypeReference<D> type);
|
||||
|
||||
}
|
||||
|
||||
@@ -75,8 +75,7 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C
|
||||
Object value = getFieldValue(dataPath);
|
||||
List<GraphQLError> errors = getFieldErrors(dataPath);
|
||||
|
||||
return new DefaultField(
|
||||
path, dataPath, (value != NO_VALUE), (value != NO_VALUE ? value : null), errors);
|
||||
return new DefaultField(path, dataPath, (value != NO_VALUE ? value : null), errors);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,18 +100,14 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C
|
||||
|
||||
private final List<GraphQLError> errors;
|
||||
|
||||
private final boolean exists;
|
||||
|
||||
@Nullable
|
||||
private final Object value;
|
||||
|
||||
public DefaultField(
|
||||
String path, List<Object> parsedPath, boolean exists, @Nullable Object value,
|
||||
List<GraphQLError> errors) {
|
||||
String path, List<Object> parsedPath, @Nullable Object value, List<GraphQLError> errors) {
|
||||
|
||||
this.path = path;
|
||||
this.parsedPath = parsedPath;
|
||||
this.exists = exists;
|
||||
this.value = value;
|
||||
this.errors = errors;
|
||||
}
|
||||
@@ -123,8 +118,8 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return (this.exists && (this.value != null || this.errors.isEmpty()));
|
||||
public boolean hasValue() {
|
||||
return (this.value != null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -160,25 +155,18 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(Class<D> elementType) {
|
||||
List<D> list = toEntity(ResolvableType.forClassWithGenerics(List.class, elementType));
|
||||
return (list != null ? list : Collections.emptyList());
|
||||
return toEntity(ResolvableType.forClassWithGenerics(List.class, elementType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(ParameterizedTypeReference<D> elementType) {
|
||||
List<D> list = toEntity(ResolvableType.forClassWithGenerics(List.class, ResolvableType.forType(elementType)));
|
||||
return (list != null ? list : Collections.emptyList());
|
||||
return toEntity(ResolvableType.forClassWithGenerics(List.class, ResolvableType.forType(elementType)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@SuppressWarnings({"unchecked", "ConstantConditions"})
|
||||
private <T> T toEntity(ResolvableType targetType) {
|
||||
if (!isValid()) {
|
||||
throw new FieldAccessException(request, DefaultClientGraphQlResponse.this, this);
|
||||
}
|
||||
|
||||
if (this.value == null) {
|
||||
return null;
|
||||
throw new FieldAccessException(request, DefaultClientGraphQlResponse.this, this);
|
||||
}
|
||||
|
||||
DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
|
||||
|
||||
@@ -199,7 +199,7 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
protected ResponseField getField(ClientGraphQlResponse response) {
|
||||
ResponseField field = response.field(this.path);
|
||||
if (!field.isValid() || !field.getErrors().isEmpty()) {
|
||||
if (!field.hasValue() || !field.getErrors().isEmpty()) {
|
||||
GraphQlRequest request = response.getRequest();
|
||||
throw new FieldAccessException(request, response, field);
|
||||
}
|
||||
@@ -220,12 +220,12 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
@Override
|
||||
public <D> Mono<D> toEntity(Class<D> entityType) {
|
||||
return this.responseMono.map(this::getField).mapNotNull(field -> field.toEntity(entityType));
|
||||
return this.responseMono.map(this::getField).map(field -> field.toEntity(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> Mono<D> toEntity(ParameterizedTypeReference<D> entityType) {
|
||||
return this.responseMono.map(this::getField).mapNotNull(field -> field.toEntity(entityType));
|
||||
return this.responseMono.map(this::getField).map(field -> field.toEntity(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -252,12 +252,12 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
@Override
|
||||
public <D> Flux<D> toEntity(Class<D> entityType) {
|
||||
return this.responseFlux.map(this::getField).mapNotNull(field -> field.toEntity(entityType));
|
||||
return this.responseFlux.map(this::getField).map(field -> field.toEntity(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> Flux<D> toEntity(ParameterizedTypeReference<D> entityType) {
|
||||
return this.responseFlux.map(this::getField).mapNotNull(field -> field.toEntity(entityType));
|
||||
return this.responseFlux.map(this::getField).map(field -> field.toEntity(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,9 +21,9 @@ import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.GraphQlResponse;
|
||||
|
||||
/**
|
||||
* An exception raised on an attempt to decode data from an
|
||||
* {@link GraphQlResponse#isValid() invalid response} or an
|
||||
* {@link ResponseField#isValid() invalid field}.
|
||||
* An exception raised on an attempt to decode data from a
|
||||
* {@link GraphQlResponse#isValid() failed response} or a field is not present,
|
||||
* or has no value, checked via {@link ResponseField#hasValue()}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
|
||||
@@ -180,11 +180,9 @@ 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, 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.
|
||||
* @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()}.
|
||||
*/
|
||||
<D> Mono<D> toEntity(Class<D> entityType);
|
||||
|
||||
@@ -196,12 +194,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 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.
|
||||
* @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
|
||||
*/
|
||||
<D> Mono<List<D>> toEntityList(Class<D> elementType);
|
||||
|
||||
@@ -221,11 +216,9 @@ 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, 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.
|
||||
* @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()}.
|
||||
*/
|
||||
<D> Flux<D> toEntity(Class<D> entityType);
|
||||
|
||||
@@ -237,12 +230,10 @@ 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 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.
|
||||
* @return lists 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
|
||||
* may also end with a range of {@link GraphQlTransportException} types.
|
||||
*/
|
||||
<D> Flux<List<D>> toEntityList(Class<D> elementType);
|
||||
|
||||
|
||||
@@ -34,17 +34,18 @@ import org.springframework.lang.Nullable;
|
||||
public interface ResponseField {
|
||||
|
||||
/**
|
||||
* Whether the field is valid. A field is invalid if:
|
||||
* Whether the field is valid and has a value.
|
||||
* <ul>
|
||||
* <li>the path doesn't exist
|
||||
* <li>it is {@code null} AND has a field error
|
||||
* <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
|
||||
* have nested, field {@link #getErrors() errors}.
|
||||
* <li>{@code "false"} means the field is {@code null} or does not exist.
|
||||
* Check for a field {@link #getError()}, which may be on the field or on a
|
||||
* parent field. The field may also be {@code null} because it is defined as
|
||||
* optional in the schema.
|
||||
* </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();
|
||||
boolean hasValue();
|
||||
|
||||
/**
|
||||
* Return the path for the field under the "data" key in the response map.
|
||||
@@ -84,24 +85,23 @@ public interface ResponseField {
|
||||
/**
|
||||
* Decode the field to an entity of the given type.
|
||||
* @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}
|
||||
* @return the decoded entity, never {@code null}
|
||||
* @throws FieldAccessException if the target field is not present or
|
||||
* has no value, checked via {@link #hasValue()}.
|
||||
*/
|
||||
@Nullable
|
||||
<D> D toEntity(Class<D> entityType);
|
||||
|
||||
/**
|
||||
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
|
||||
*/
|
||||
@Nullable
|
||||
<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 decoded list of entities, possibly empty
|
||||
* @throws FieldAccessException if "this" field is not {@link #isValid() valid}
|
||||
* @throws FieldAccessException if the target field is not present or
|
||||
* has no value, checked via {@link #hasValue()}.
|
||||
*/
|
||||
<D> List<D> toEntityList(Class<D> elementType);
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.isValid()).isFalse();
|
||||
assertThat(response.field("me").isValid()).isFalse();
|
||||
assertThat(response.field("me").hasValue()).isFalse();
|
||||
|
||||
assertThatThrownBy(() -> response.field("me").toEntity(MovieCharacter.class))
|
||||
.isInstanceOf(FieldAccessException.class);
|
||||
@@ -187,7 +187,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
|
||||
.isTrue();
|
||||
|
||||
ResponseField field = response.field("me");
|
||||
assertThat(field.isValid()).isTrue();
|
||||
assertThat(field.hasValue()).isTrue();
|
||||
assertThat(field.getErrors()).hasSize(1);
|
||||
assertThat(field.getErrors().get(0).getPath()).containsExactly("me", "name");
|
||||
assertThat(field.toEntity(MovieCharacter.class))
|
||||
@@ -195,7 +195,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
|
||||
.isNotNull();
|
||||
|
||||
ResponseField nameField = response.field("me.name");
|
||||
assertThat(nameField.isValid()).isFalse();
|
||||
assertThat(nameField.hasValue()).isFalse();
|
||||
assertThat(nameField.getError()).isNotNull();
|
||||
assertThat(nameField.getError().getPath()).containsExactly("me", "name");
|
||||
assertThatThrownBy(() -> nameField.toEntity(String.class))
|
||||
@@ -203,7 +203,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
|
||||
.isInstanceOf(FieldAccessException.class);
|
||||
|
||||
ResponseField nonExistingField = response.field("me.name.other");
|
||||
assertThat(nonExistingField.isValid()).isFalse();
|
||||
assertThat(nonExistingField.hasValue()).isFalse();
|
||||
assertThat(nameField.getError()).isNotNull();
|
||||
assertThat(nameField.getError().getPath()).containsExactly("me", "name");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user