Extract GraphQlResponseField in the top-level package

GraphQlResponseField is now extracted as a super type at the top-level
package and is exposed from GraphQlResponse. ClientGraphQlResponseField
extends this to provide decoding options.

The change ensures consistency with both GraphQlResponseField and
GraphQlResponseError accessible through GraphQlResponse, also making
both available for client and server side handling.

See gh-10
This commit is contained in:
rstoyanchev
2022-03-18 18:16:59 +00:00
parent cdd8b67fed
commit 982cbbaa45
12 changed files with 307 additions and 232 deletions

View File

@@ -0,0 +1,182 @@
/*
* 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;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Default implementation of {@link GraphQlResponseField}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public class DefaultGraphQlResponseField implements GraphQlResponseField {
private final GraphQlResponse response;
private final String path;
private final List<Object> parsedPath;
@Nullable
private final Object value;
private final List<GraphQlResponseError> fieldErrors;
protected DefaultGraphQlResponseField(GraphQlResponse response, String path) {
this.response = response;
this.path = path;
this.parsedPath = parsePath(path);
this.value = initFieldValue(this.parsedPath, response);
this.fieldErrors = initFieldErrors(path, response);
}
private static List<Object> parsePath(String path) {
if (!StringUtils.hasText(path)) {
return Collections.emptyList();
}
String invalidPathMessage = "Invalid path: '" + path + "'";
List<Object> dataPath = new ArrayList<>();
StringBuilder sb = new StringBuilder();
boolean readingIndex = false;
for (int i = 0; i < path.length(); i++) {
char c = path.charAt(i);
switch (c) {
case '.':
case '[':
Assert.isTrue(!readingIndex, invalidPathMessage);
break;
case ']':
i++;
Assert.isTrue(readingIndex, invalidPathMessage);
Assert.isTrue(i == path.length() || path.charAt(i) == '.', invalidPathMessage);
break;
default:
sb.append(c);
if (i < path.length() - 1) {
continue;
}
}
String token = sb.toString();
Assert.hasText(token, invalidPathMessage);
dataPath.add(readingIndex ? Integer.parseInt(token) : token);
sb.delete(0, sb.length());
readingIndex = (c == '[');
}
return dataPath;
}
@Nullable
private static Object initFieldValue(List<Object> path, GraphQlResponse response) {
Object value = (response.isValid() ? response.getData() : null);
for (Object segment : path) {
if (value == null) {
return null;
}
if (segment instanceof String) {
Assert.isTrue(value instanceof Map, () -> "Invalid path " + path + ", data: " + response.getData());
value = ((Map<?, ?>) value).getOrDefault(segment, null);
}
else {
Assert.isTrue(value instanceof List, () -> "Invalid path " + path + ", data: " + response.getData());
int index = (int) segment;
value = (index < ((List<?>) value).size() ? ((List<?>) value).get(index) : null);
}
}
return value;
}
/**
* Return field errors whose path starts with the given field path.
* @param path the field path to match
* @return errors whose path starts with the dataPath
*/
private static List<GraphQlResponseError> initFieldErrors(String path, GraphQlResponse response) {
if (path.isEmpty() || response.getErrors().isEmpty()) {
return Collections.emptyList();
}
return response.getErrors().stream()
.filter(error -> {
String errorPath = error.getPath();
return !errorPath.isEmpty() && (errorPath.startsWith(path) || path.startsWith(errorPath));
})
.collect(Collectors.toList());
}
@SuppressWarnings("unchecked")
protected <R extends GraphQlResponse> R getResponse() {
return (R) this.response;
}
@Override
public String getPath() {
return this.path;
}
@Override
public List<Object> getParsedPath() {
return this.parsedPath;
}
@Override
public boolean hasValue() {
return (this.value != null);
}
@SuppressWarnings("unchecked")
@Override
public <T> T getValue() {
return (T) this.value;
}
@Override
public GraphQlResponseError getError() {
if (!hasValue()) {
if (!this.fieldErrors.isEmpty()) {
return this.fieldErrors.get(0);
}
if (!this.response.getErrors().isEmpty()) {
return this.response.getErrors().get(0);
}
// No errors, set to null by DataFetcher
}
return null;
}
@Override
public List<GraphQlResponseError> getErrors() {
return this.fieldErrors;
}
}

View File

@@ -65,6 +65,26 @@ public interface GraphQlResponse {
*/
List<GraphQlResponseError> getErrors();
/**
* 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.
* <p>Example paths:
* <pre>
* "hero"
* "hero.name"
* "hero.friends"
* "hero.friends[2]"
* "hero.friends[2].name"
* </pre>
* @param path relative to the "data" key
* @return representation for the field with further options to inspect or
* decode its value; use {@link GraphQlResponseField#hasValue()} to check if
* the field actually exists and has a value.
*/
default GraphQlResponseField field(String path) {
return new DefaultGraphQlResponseField(this, path);
}
/**
* Return implementor specific, protocol extensions, if any.
*/

View File

@@ -14,19 +14,17 @@
* limitations under the License.
*/
package org.springframework.graphql.client;
package org.springframework.graphql;
import java.util.List;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlResponseError;
import org.springframework.graphql.client.ClientGraphQlResponse;
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.
* Representation for a field in a GraphQL response, with options to examine
* the field value and errors.
*
* @author Rossen Stoyanchev
* @since 1.0.0
@@ -58,8 +56,8 @@ public interface GraphQlResponseField {
List<Object> getParsedPath();
/**
* Return the field value without any decoding.
* @param <T> the expected value type, e.g. Map, List, or a scalar type.
* Return the raw field value, e.g. Map, List, or a scalar type.
* @param <T> the expected value type to cast to
* @return the value
*/
@Nullable
@@ -98,32 +96,4 @@ public interface GraphQlResponseField {
*/
List<GraphQlResponseError> getErrors();
/**
* Decode the field to an entity of the given type.
* @param entityType the type to convert to
* @return the decoded entity, never {@code null}
* @throws FieldAccessException if the target field is not present or
* has no value, checked via {@link #hasValue()}.
*/
<D> D toEntity(Class<D> entityType);
/**
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
*/
<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 the target field is not present or
* has no value, checked via {@link #hasValue()}.
*/
<D> List<D> toEntityList(Class<D> elementType);
/**
* Variant of {@link #toEntityList(Class)} with {@link ParameterizedTypeReference}.
*/
<D> List<D> toEntityList(ParameterizedTypeReference<D> elementType);
}

View File

@@ -36,22 +36,9 @@ public interface ClientGraphQlResponse extends GraphQlResponse {
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.
* <p>Example paths:
* <pre>
* "hero"
* "hero.name"
* "hero.friends"
* "hero.friends[2]"
* "hero.friends[2].name"
* </pre>
* @param path relative to the "data" key
* @return representation for the field with further options to inspect or
* decode its value; use {@link GraphQlResponseField#hasValue()} to check if
* the field actually exists and has a value.
* {@inheritDoc}
*/
GraphQlResponseField field(String path);
ClientGraphQlResponseField field(String path);
/**
* Decode the full response map to the given target type.

View File

@@ -0,0 +1,61 @@
/*
* 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 org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.GraphQlResponseField;
/**
* Extends {@link GraphQlResponseField} to add options for decoding the field value.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public interface ClientGraphQlResponseField extends GraphQlResponseField {
/**
* Decode the field to an entity of the given type.
* @param entityType the type to convert to
* @return the decoded entity, never {@code null}
* @throws FieldAccessException if the target field is not present or
* has no value, checked via {@link #hasValue()}.
*/
<D> D toEntity(Class<D> entityType);
/**
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
*/
<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 the target field is not present or
* has no value, checked via {@link #hasValue()}.
*/
<D> List<D> toEntityList(Class<D> elementType);
/**
* Variant of {@link #toEntityList(Class)} with {@link ParameterizedTypeReference}.
*/
<D> List<D> toEntityList(ParameterizedTypeReference<D> elementType);
}

View File

@@ -16,21 +16,11 @@
package org.springframework.graphql.client;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.GraphQlResponseError;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
@@ -74,86 +64,8 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C
@Override
public GraphQlResponseField field(String path) {
List<Object> parsedPath = parsePath(path);
return new DefaultGraphQlResponseField(this, path, parsedPath, getValue(parsedPath), getFieldErrors(path));
}
private static List<Object> parsePath(String path) {
if (!StringUtils.hasText(path)) {
return Collections.emptyList();
}
String invalidPathMessage = "Invalid path: '" + path + "'";
List<Object> dataPath = new ArrayList<>();
StringBuilder sb = new StringBuilder();
boolean readingIndex = false;
for (int i = 0; i < path.length(); i++) {
char c = path.charAt(i);
switch (c) {
case '.':
case '[':
Assert.isTrue(!readingIndex, invalidPathMessage);
break;
case ']':
i++;
Assert.isTrue(readingIndex, invalidPathMessage);
Assert.isTrue(i == path.length() || path.charAt(i) == '.', invalidPathMessage);
break;
default:
sb.append(c);
if (i < path.length() - 1) {
continue;
}
}
String token = sb.toString();
Assert.hasText(token, invalidPathMessage);
dataPath.add(readingIndex ? Integer.parseInt(token) : token);
sb.delete(0, sb.length());
readingIndex = (c == '[');
}
return dataPath;
}
@Nullable
private Object getValue(List<Object> path) {
Object value = (isValid() ? getData() : null);
for (Object segment : path) {
if (value == null) {
return null;
}
if (segment instanceof String) {
Assert.isTrue(value instanceof Map, () -> "Invalid path " + path + ", data: " + getData());
value = ((Map<?, ?>) value).getOrDefault(segment, null);
}
else {
Assert.isTrue(value instanceof List, () -> "Invalid path " + path + ", data: " + getData());
int index = (int) segment;
value = (index < ((List<?>) value).size() ? ((List<?>) value).get(index) : null);
}
}
return value;
}
/**
* Return field errors whose path starts with the given field path.
* @param path the field path to match
* @return errors whose path starts with the dataPath
*/
private List<GraphQlResponseError> getFieldErrors(String path) {
if (path.isEmpty()) {
return Collections.emptyList();
}
return getErrors().stream()
.filter(error -> {
String errorPath = error.getPath();
return !errorPath.isEmpty() && (errorPath.startsWith(path) || path.startsWith(errorPath));
})
.collect(Collectors.toList());
public ClientGraphQlResponseField field(String path) {
return new DefaultClientGraphQlResponseField(this, path);
}
@Override

View File

@@ -27,84 +27,25 @@ import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.graphql.GraphQlResponseError;
import org.springframework.lang.Nullable;
import org.springframework.graphql.DefaultGraphQlResponseField;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* Default implementation of {@link GraphQlResponseField}.
* Default implementation of {@link ClientGraphQlResponseField}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
final class DefaultGraphQlResponseField implements GraphQlResponseField {
private final DefaultClientGraphQlResponse response;
private final String path;
private final List<Object> parsedPath;
@Nullable
private final Object value;
private final List<GraphQlResponseError> fieldErrors;
final class DefaultClientGraphQlResponseField extends DefaultGraphQlResponseField implements ClientGraphQlResponseField {
DefaultGraphQlResponseField(
DefaultClientGraphQlResponse response, String path, List<Object> parsedPath,
@Nullable Object value, List<GraphQlResponseError> errors) {
this.response = response;
this.path = path;
this.parsedPath = parsedPath;
this.value = value;
this.fieldErrors = errors;
DefaultClientGraphQlResponseField(DefaultClientGraphQlResponse response, String path) {
super(response, path);
}
@Override
public String getPath() {
return this.path;
}
@Override
public List<Object> getParsedPath() {
return this.parsedPath;
}
@Override
public boolean hasValue() {
return (this.value != null);
}
@SuppressWarnings("unchecked")
@Override
public <T> T getValue() {
return (T) this.value;
}
@Override
public GraphQlResponseError getError() {
if (!hasValue()) {
if (!this.fieldErrors.isEmpty()) {
return this.fieldErrors.get(0);
}
if (!this.response.getErrors().isEmpty()) {
return this.response.getErrors().get(0);
}
// No errors, set to null by DataFetcher
}
return null;
}
@Override
public List<GraphQlResponseError> getErrors() {
return this.fieldErrors;
}
@Override
public <D> D toEntity(Class<D> entityType) {
return toEntity(ResolvableType.forType(entityType));
@@ -127,18 +68,19 @@ final class DefaultGraphQlResponseField implements GraphQlResponseField {
@SuppressWarnings({"unchecked", "ConstantConditions"})
private <T> T toEntity(ResolvableType targetType) {
if (this.value == null) {
throw new FieldAccessException(this.response, this);
DefaultClientGraphQlResponse response = getResponse();
if (!hasValue()) {
throw new FieldAccessException(response, this);
}
DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
MimeType mimeType = MimeTypeUtils.APPLICATION_JSON;
Map<String, Object> hints = Collections.emptyMap();
DataBuffer buffer = ((Encoder<T>) this.response.getEncoder()).encodeValue(
(T) this.value, bufferFactory, ResolvableType.forInstance(this.value), mimeType, hints);
DataBuffer buffer = ((Encoder<T>) response.getEncoder()).encodeValue(
(T) getValue(), bufferFactory, ResolvableType.forInstance(getValue()), mimeType, hints);
return ((Decoder<T>) this.response.getDecoder()).decode(buffer, targetType, mimeType, hints);
return ((Decoder<T>) response.getDecoder()).decode(buffer, targetType, mimeType, hints);
}
}

View File

@@ -203,8 +203,8 @@ final class DefaultGraphQlClient implements GraphQlClient {
* @throws FieldAccessException for invalid response or failed field
*/
@Nullable
protected GraphQlResponseField getValidField(ClientGraphQlResponse response) {
GraphQlResponseField field = response.field(this.path);
protected ClientGraphQlResponseField getValidField(ClientGraphQlResponse response) {
ClientGraphQlResponseField field = response.field(this.path);
if (!response.isValid() || field.getError() != null) {
throw new FieldAccessException(response, field);
}
@@ -236,7 +236,7 @@ final class DefaultGraphQlClient implements GraphQlClient {
@Override
public <D> Mono<List<D>> toEntityList(Class<D> elementType) {
return this.responseMono.map(response -> {
GraphQlResponseField field = getValidField(response);
ClientGraphQlResponseField field = getValidField(response);
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
});
}
@@ -244,7 +244,7 @@ final class DefaultGraphQlClient implements GraphQlClient {
@Override
public <D> Mono<List<D>> toEntityList(ParameterizedTypeReference<D> elementType) {
return this.responseMono.map(response -> {
GraphQlResponseField field = getValidField(response);
ClientGraphQlResponseField field = getValidField(response);
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
});
}
@@ -274,7 +274,7 @@ final class DefaultGraphQlClient implements GraphQlClient {
@Override
public <D> Flux<List<D>> toEntityList(Class<D> elementType) {
return this.responseFlux.map(response -> {
GraphQlResponseField field = getValidField(response);
ClientGraphQlResponseField field = getValidField(response);
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
});
}
@@ -282,7 +282,7 @@ final class DefaultGraphQlClient implements GraphQlClient {
@Override
public <D> Flux<List<D>> toEntityList(ParameterizedTypeReference<D> elementType) {
return this.responseFlux.map(response -> {
GraphQlResponseField field = getValidField(response);
ClientGraphQlResponseField field = getValidField(response);
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
});
}

View File

@@ -22,7 +22,8 @@ import org.springframework.graphql.GraphQlResponse;
/**
* 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 GraphQlResponseField#hasValue()}.
* or has no value, checked via
* {@link org.springframework.graphql.GraphQlResponseField#hasValue()}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
@@ -32,19 +33,19 @@ public class FieldAccessException extends GraphQlClientException {
private final ClientGraphQlResponse response;
private final GraphQlResponseField field;
private final ClientGraphQlResponseField field;
/**
* Constructor with the request and response, and the accessed field.
*/
public FieldAccessException(ClientGraphQlResponse response, GraphQlResponseField field) {
public FieldAccessException(ClientGraphQlResponse response, ClientGraphQlResponseField field) {
super(initDefaultMessage(field), null, response.getRequest());
this.response = response;
this.field = field;
}
private static String initDefaultMessage(GraphQlResponseField field) {
private static String initDefaultMessage(ClientGraphQlResponseField field) {
return "Invalid field '" + field.getPath() + "', errors: " + field.getErrors();
}
@@ -59,7 +60,7 @@ public class FieldAccessException extends GraphQlClientException {
/**
* Return the field that needed to be accessed.
*/
public GraphQlResponseField getField() {
public ClientGraphQlResponseField getField() {
return this.field;
}

View File

@@ -185,7 +185,7 @@ public interface GraphQlClient {
* the field is {@code null} without errors, or ends with
* {@link FieldAccessException} for an invalid response or a failed field
* @see GraphQlResponse#isValid()
* @see GraphQlResponseField#getError()
* @see org.springframework.graphql.GraphQlResponseField#getError()
*/
<D> Mono<D> toEntity(Class<D> entityType);
@@ -201,7 +201,7 @@ public interface GraphQlClient {
* empty list, or ends with {@link FieldAccessException} if the target
* field is not present or has no value.
* @see GraphQlResponse#isValid()
* @see GraphQlResponseField#getError()
* @see org.springframework.graphql.GraphQlResponseField#getError()
*/
<D> Mono<List<D>> toEntityList(Class<D> elementType);
@@ -226,7 +226,7 @@ public interface GraphQlClient {
* {@link FieldAccessException} for an invalid response or a failed field.
* May also end with a {@link GraphQlTransportException}.
* @see GraphQlResponse#isValid()
* @see GraphQlResponseField#getError()
* @see org.springframework.graphql.GraphQlResponseField#getError()
*/
<D> Flux<D> toEntity(Class<D> entityType);
@@ -243,7 +243,7 @@ public interface GraphQlClient {
* {@link FieldAccessException} for an invalid response or a failed field.
* May also end with a {@link GraphQlTransportException}.
* @see GraphQlResponse#isValid()
* @see GraphQlResponseField#getError()
* @see org.springframework.graphql.GraphQlResponseField#getError()
*/
<D> Flux<List<D>> toEntityList(Class<D> elementType);

View File

@@ -127,7 +127,7 @@ public class DefaultGraphQlClientResponseTests {
GraphQLError error2 = createError("/me/friends", "fail-me-friends");
GraphQLError error3 = createError("/me/friends[0]/name", "fail-me-friends-name");
GraphQlResponseField field = getField(path, error0, error1, error2, error3);
ClientGraphQlResponseField field = getField(path, error0, error1, error2, error3);
List<GraphQlResponseError> errors = field.getErrors();
assertThat(errors).hasSize(3);
@@ -144,13 +144,13 @@ public class DefaultGraphQlClientResponseTests {
return builder.build();
}
private GraphQlResponseField getField(String path, String dataJson) throws Exception {
private ClientGraphQlResponseField getField(String path, String dataJson) throws Exception {
Map<?, ?> dataMap = mapper.readValue(dataJson, Map.class);
ClientGraphQlResponse response = creatResponse(Collections.singletonMap("data", dataMap));
return response.field(path);
}
private GraphQlResponseField getField(String path, GraphQLError... errors) {
private ClientGraphQlResponseField getField(String path, GraphQLError... errors) {
List<?> list = Arrays.stream(errors).map(GraphQLError::toSpecification).collect(Collectors.toList());
ClientGraphQlResponse response = creatResponse(Collections.singletonMap("errors", list));
return response.field(path);

View File

@@ -188,7 +188,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
.as("Partial response with field errors should be considered valid")
.isTrue();
GraphQlResponseField field = response.field("me");
ClientGraphQlResponseField field = response.field("me");
assertThat(field.hasValue()).isTrue();
assertThat(field.getErrors()).hasSize(1);
assertThat(field.getErrors().get(0).getParsedPath()).containsExactly("me", "name");
@@ -196,7 +196,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
.as("Decoding with nested field error should not be precluded")
.isNotNull();
GraphQlResponseField nameField = response.field("me.name");
ClientGraphQlResponseField nameField = response.field("me.name");
assertThat(nameField.hasValue()).isFalse();
assertThat(nameField.getError()).isNotNull();
assertThat(nameField.getError().getParsedPath()).containsExactly("me", "name");
@@ -204,7 +204,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
.as("Decoding field null with direct field error should be rejected")
.isInstanceOf(FieldAccessException.class);
GraphQlResponseField nonExistingField = response.field("me.name.other");
ClientGraphQlResponseField nonExistingField = response.field("me.name.other");
assertThat(nonExistingField.hasValue()).isFalse();
assertThat(nameField.getError()).isNotNull();
assertThat(nameField.getError().getParsedPath()).containsExactly("me", "name");