Skip to content

Commit 982cbba

Browse files
committed
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
1 parent cdd8b67 commit 982cbba

12 files changed

+307
-232
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.stream.Collectors;
24+
25+
import org.springframework.lang.Nullable;
26+
import org.springframework.util.Assert;
27+
import org.springframework.util.StringUtils;
28+
29+
30+
/**
31+
* Default implementation of {@link GraphQlResponseField}.
32+
*
33+
* @author Rossen Stoyanchev
34+
* @since 1.0.0
35+
*/
36+
public class DefaultGraphQlResponseField implements GraphQlResponseField {
37+
38+
private final GraphQlResponse response;
39+
40+
private final String path;
41+
42+
private final List<Object> parsedPath;
43+
44+
@Nullable
45+
private final Object value;
46+
47+
private final List<GraphQlResponseError> fieldErrors;
48+
49+
50+
protected DefaultGraphQlResponseField(GraphQlResponse response, String path) {
51+
52+
this.response = response;
53+
this.path = path;
54+
this.parsedPath = parsePath(path);
55+
this.value = initFieldValue(this.parsedPath, response);
56+
this.fieldErrors = initFieldErrors(path, response);
57+
}
58+
59+
private static List<Object> parsePath(String path) {
60+
if (!StringUtils.hasText(path)) {
61+
return Collections.emptyList();
62+
}
63+
64+
String invalidPathMessage = "Invalid path: '" + path + "'";
65+
List<Object> dataPath = new ArrayList<>();
66+
67+
StringBuilder sb = new StringBuilder();
68+
boolean readingIndex = false;
69+
70+
for (int i = 0; i < path.length(); i++) {
71+
char c = path.charAt(i);
72+
switch (c) {
73+
case '.':
74+
case '[':
75+
Assert.isTrue(!readingIndex, invalidPathMessage);
76+
break;
77+
case ']':
78+
i++;
79+
Assert.isTrue(readingIndex, invalidPathMessage);
80+
Assert.isTrue(i == path.length() || path.charAt(i) == '.', invalidPathMessage);
81+
break;
82+
default:
83+
sb.append(c);
84+
if (i < path.length() - 1) {
85+
continue;
86+
}
87+
}
88+
String token = sb.toString();
89+
Assert.hasText(token, invalidPathMessage);
90+
dataPath.add(readingIndex ? Integer.parseInt(token) : token);
91+
sb.delete(0, sb.length());
92+
93+
readingIndex = (c == '[');
94+
}
95+
96+
return dataPath;
97+
}
98+
99+
@Nullable
100+
private static Object initFieldValue(List<Object> path, GraphQlResponse response) {
101+
Object value = (response.isValid() ? response.getData() : null);
102+
for (Object segment : path) {
103+
if (value == null) {
104+
return null;
105+
}
106+
if (segment instanceof String) {
107+
Assert.isTrue(value instanceof Map, () -> "Invalid path " + path + ", data: " + response.getData());
108+
value = ((Map<?, ?>) value).getOrDefault(segment, null);
109+
}
110+
else {
111+
Assert.isTrue(value instanceof List, () -> "Invalid path " + path + ", data: " + response.getData());
112+
int index = (int) segment;
113+
value = (index < ((List<?>) value).size() ? ((List<?>) value).get(index) : null);
114+
}
115+
}
116+
return value;
117+
}
118+
119+
/**
120+
* Return field errors whose path starts with the given field path.
121+
* @param path the field path to match
122+
* @return errors whose path starts with the dataPath
123+
*/
124+
private static List<GraphQlResponseError> initFieldErrors(String path, GraphQlResponse response) {
125+
if (path.isEmpty() || response.getErrors().isEmpty()) {
126+
return Collections.emptyList();
127+
}
128+
return response.getErrors().stream()
129+
.filter(error -> {
130+
String errorPath = error.getPath();
131+
return !errorPath.isEmpty() && (errorPath.startsWith(path) || path.startsWith(errorPath));
132+
})
133+
.collect(Collectors.toList());
134+
}
135+
136+
137+
@SuppressWarnings("unchecked")
138+
protected <R extends GraphQlResponse> R getResponse() {
139+
return (R) this.response;
140+
}
141+
142+
@Override
143+
public String getPath() {
144+
return this.path;
145+
}
146+
147+
@Override
148+
public List<Object> getParsedPath() {
149+
return this.parsedPath;
150+
}
151+
152+
@Override
153+
public boolean hasValue() {
154+
return (this.value != null);
155+
}
156+
157+
@SuppressWarnings("unchecked")
158+
@Override
159+
public <T> T getValue() {
160+
return (T) this.value;
161+
}
162+
163+
@Override
164+
public GraphQlResponseError getError() {
165+
if (!hasValue()) {
166+
if (!this.fieldErrors.isEmpty()) {
167+
return this.fieldErrors.get(0);
168+
}
169+
if (!this.response.getErrors().isEmpty()) {
170+
return this.response.getErrors().get(0);
171+
}
172+
// No errors, set to null by DataFetcher
173+
}
174+
return null;
175+
}
176+
177+
@Override
178+
public List<GraphQlResponseError> getErrors() {
179+
return this.fieldErrors;
180+
}
181+
182+
}

spring-graphql/src/main/java/org/springframework/graphql/GraphQlResponse.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,26 @@ public interface GraphQlResponse {
6565
*/
6666
List<GraphQlResponseError> getErrors();
6767

68+
/**
69+
* Navigate to the given path under the "data" key of the response map where
70+
* the path is a dot-separated string with optional array indexes.
71+
* <p>Example paths:
72+
* <pre>
73+
* "hero"
74+
* "hero.name"
75+
* "hero.friends"
76+
* "hero.friends[2]"
77+
* "hero.friends[2].name"
78+
* </pre>
79+
* @param path relative to the "data" key
80+
* @return representation for the field with further options to inspect or
81+
* decode its value; use {@link GraphQlResponseField#hasValue()} to check if
82+
* the field actually exists and has a value.
83+
*/
84+
default GraphQlResponseField field(String path) {
85+
return new DefaultGraphQlResponseField(this, path);
86+
}
87+
6888
/**
6989
* Return implementor specific, protocol extensions, if any.
7090
*/

spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlResponseField.java renamed to spring-graphql/src/main/java/org/springframework/graphql/GraphQlResponseField.java

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,17 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.graphql.client;
18-
17+
package org.springframework.graphql;
1918

2019
import java.util.List;
2120

22-
import org.springframework.core.ParameterizedTypeReference;
23-
import org.springframework.graphql.GraphQlResponse;
24-
import org.springframework.graphql.GraphQlResponseError;
21+
import org.springframework.graphql.client.ClientGraphQlResponse;
2522
import org.springframework.lang.Nullable;
2623

24+
2725
/**
28-
* Representation for a field in a GraphQL response, with options to examine its
29-
* value and errors, and to decode it.
26+
* Representation for a field in a GraphQL response, with options to examine
27+
* the field value and errors.
3028
*
3129
* @author Rossen Stoyanchev
3230
* @since 1.0.0
@@ -58,8 +56,8 @@ public interface GraphQlResponseField {
5856
List<Object> getParsedPath();
5957

6058
/**
61-
* Return the field value without any decoding.
62-
* @param <T> the expected value type, e.g. Map, List, or a scalar type.
59+
* Return the raw field value, e.g. Map, List, or a scalar type.
60+
* @param <T> the expected value type to cast to
6361
* @return the value
6462
*/
6563
@Nullable
@@ -98,32 +96,4 @@ public interface GraphQlResponseField {
9896
*/
9997
List<GraphQlResponseError> getErrors();
10098

101-
/**
102-
* Decode the field to an entity of the given type.
103-
* @param entityType the type to convert to
104-
* @return the decoded entity, never {@code null}
105-
* @throws FieldAccessException if the target field is not present or
106-
* has no value, checked via {@link #hasValue()}.
107-
*/
108-
<D> D toEntity(Class<D> entityType);
109-
110-
/**
111-
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
112-
*/
113-
<D> D toEntity(ParameterizedTypeReference<D> entityType);
114-
115-
/**
116-
* Decode the field to a list of entities with the given type.
117-
* @param elementType the type of elements in the list
118-
* @return the decoded list of entities, possibly empty
119-
* @throws FieldAccessException if the target field is not present or
120-
* has no value, checked via {@link #hasValue()}.
121-
*/
122-
<D> List<D> toEntityList(Class<D> elementType);
123-
124-
/**
125-
* Variant of {@link #toEntityList(Class)} with {@link ParameterizedTypeReference}.
126-
*/
127-
<D> List<D> toEntityList(ParameterizedTypeReference<D> elementType);
128-
12999
}

spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlResponse.java

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,9 @@ public interface ClientGraphQlResponse extends GraphQlResponse {
3636
GraphQlRequest getRequest();
3737

3838
/**
39-
* Navigate to the given path under the "data" key of the response map where
40-
* the path is a dot-separated string with optional array indexes.
41-
* <p>Example paths:
42-
* <pre>
43-
* "hero"
44-
* "hero.name"
45-
* "hero.friends"
46-
* "hero.friends[2]"
47-
* "hero.friends[2].name"
48-
* </pre>
49-
* @param path relative to the "data" key
50-
* @return representation for the field with further options to inspect or
51-
* decode its value; use {@link GraphQlResponseField#hasValue()} to check if
52-
* the field actually exists and has a value.
39+
* {@inheritDoc}
5340
*/
54-
GraphQlResponseField field(String path);
41+
ClientGraphQlResponseField field(String path);
5542

5643
/**
5744
* Decode the full response map to the given target type.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.client;
18+
19+
20+
import java.util.List;
21+
22+
import org.springframework.core.ParameterizedTypeReference;
23+
import org.springframework.graphql.GraphQlResponseField;
24+
25+
/**
26+
* Extends {@link GraphQlResponseField} to add options for decoding the field value.
27+
*
28+
* @author Rossen Stoyanchev
29+
* @since 1.0.0
30+
*/
31+
public interface ClientGraphQlResponseField extends GraphQlResponseField {
32+
33+
/**
34+
* Decode the field to an entity of the given type.
35+
* @param entityType the type to convert to
36+
* @return the decoded entity, never {@code null}
37+
* @throws FieldAccessException if the target field is not present or
38+
* has no value, checked via {@link #hasValue()}.
39+
*/
40+
<D> D toEntity(Class<D> entityType);
41+
42+
/**
43+
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
44+
*/
45+
<D> D toEntity(ParameterizedTypeReference<D> entityType);
46+
47+
/**
48+
* Decode the field to a list of entities with the given type.
49+
* @param elementType the type of elements in the list
50+
* @return the decoded list of entities, possibly empty
51+
* @throws FieldAccessException if the target field is not present or
52+
* has no value, checked via {@link #hasValue()}.
53+
*/
54+
<D> List<D> toEntityList(Class<D> elementType);
55+
56+
/**
57+
* Variant of {@link #toEntityList(Class)} with {@link ParameterizedTypeReference}.
58+
*/
59+
<D> List<D> toEntityList(ParameterizedTypeReference<D> elementType);
60+
61+
}

0 commit comments

Comments
 (0)