Skip to content

Commit

Permalink
Extract GraphQlResponseField in the top-level package
Browse files Browse the repository at this point in the history
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
  • Loading branch information
rstoyanchev committed Mar 18, 2022
1 parent cdd8b67 commit 982cbba
Show file tree
Hide file tree
Showing 12 changed files with 307 additions and 232 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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);

}
Loading

0 comments on commit 982cbba

Please sign in to comment.