Expose map of "extensions" in GraphQlRequest

This commit adds support for protocol, vendor specific protocol
extensions in the GraphQL client requests.

Closes gh-371
This commit is contained in:
Brian Clozel
2022-05-16 11:32:32 +02:00
parent a2c038e933
commit c2c6ae6547
18 changed files with 159 additions and 19 deletions

View File

@@ -75,7 +75,7 @@ abstract class AbstractDirectGraphQlTransport implements GraphQlTransport {
private ExecutionGraphQlRequest toExecutionRequest(GraphQlRequest request) {
return new DefaultExecutionGraphQlRequest(
request.getDocument(), request.getOperationName(), request.getVariables(),
request.getDocument(), request.getOperationName(), request.getVariables(), request.getExtensions(),
idGenerator.generateId().toString(), null);
}

View File

@@ -125,6 +125,8 @@ final class DefaultGraphQlTester implements GraphQlTester {
private final Map<String, Object> variables = new LinkedHashMap<>();
private final Map<String, Object> extensions = new LinkedHashMap<>();
private DefaultRequest(String document) {
Assert.notNull(document, "`document` is required");
this.document = document;
@@ -142,6 +144,12 @@ final class DefaultGraphQlTester implements GraphQlTester {
return this;
}
@Override
public DefaultRequest extension(String name, Object value) {
this.extensions.put(name, value);
return this;
}
@SuppressWarnings("ConstantConditions")
@Override
public Response execute() {
@@ -159,7 +167,7 @@ final class DefaultGraphQlTester implements GraphQlTester {
}
private GraphQlRequest request() {
return new DefaultGraphQlRequest(this.document, this.operationName, this.variables);
return new DefaultGraphQlRequest(this.document, this.operationName, this.variables, this.extensions);
}
private DefaultResponse mapResponse(GraphQlResponse response, GraphQlRequest request) {

View File

@@ -149,6 +149,15 @@ public interface GraphQlTester {
*/
T variable(String name, @Nullable Object value);
/**
* Add a variable.
* @param name the variable name
* @param value the variable value, possibly {@code null} since GraphQL
* supports providing null value vs not providing a value at all.
* @return this request spec
*/
T extension(String name, @Nullable Object value);
/**
* Execute the GraphQL request and return a spec for further inspection of
* response data and errors.

View File

@@ -217,6 +217,23 @@ public class GraphQlTesterTests extends GraphQlTesterTestSupport {
assertThat(request.getVariables()).containsEntry("keyOnly", null);
}
@Test
void protocolExtensions() {
String document = "{me {name, friends}}";
getGraphQlService().setDataAsJson(document, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
graphQlTester().document(document)
.extension("firstExt", Collections.singletonMap("key", "value"))
.extension("secondExt", "value")
.execute();
ExecutionGraphQlRequest request = getGraphQlService().getGraphQlRequest();
assertThat(request.getDocument()).contains(document);
assertThat(request.getExtensions()).hasSize(2);
assertThat(request.getExtensions()).containsEntry("firstExt", Collections.singletonMap("key", "value"))
.containsEntry("secondExt", "value");
}
@Test
void errorsEmptyOnExecuteAndVerify() {

View File

@@ -53,6 +53,11 @@ public interface GraphQlRequest {
*/
Map<String, Object> getVariables();
/**
* Return implementor specific, protocol extensions, if any.
*/
Map<String, Object> getExtensions();
/**
* Convert the request to a {@link Map} as defined in
* <a href="https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md">GraphQL over HTTP</a> and

View File

@@ -35,10 +35,11 @@ final class DefaultClientGraphQlRequest extends DefaultGraphQlRequest implements
DefaultClientGraphQlRequest(
String document, @Nullable String operationName, Map<String, Object> variables,
String document, @Nullable String operationName,
Map<String, Object> variables, Map<String, Object> extensions,
Map<String, Object> attributes) {
super(document, operationName, variables);
super(document, operationName, variables, extensions);
this.attributes.putAll(attributes);
}

View File

@@ -94,6 +94,8 @@ final class DefaultGraphQlClient implements GraphQlClient {
private final Map<String, Object> attributes = new LinkedHashMap<>();
private final Map<String, Object> extensions = new LinkedHashMap<>();
DefaultRequestSpec(Mono<String> documentMono) {
Assert.notNull(documentMono, "'document' is required");
this.documentMono = documentMono;
@@ -117,6 +119,18 @@ final class DefaultGraphQlClient implements GraphQlClient {
return this;
}
@Override
public RequestSpec extension(String name, Object value) {
this.extensions.put(name, value);
return this;
}
@Override
public RequestSpec extensions(Map<String, Object> extensions) {
this.extensions.putAll(extensions);
return this;
}
@Override
public RequestSpec attribute(String name, Object value) {
this.attributes.put(name, value);
@@ -157,7 +171,7 @@ final class DefaultGraphQlClient implements GraphQlClient {
private Mono<ClientGraphQlRequest> initRequest() {
return this.documentMono.map(document ->
new DefaultClientGraphQlRequest(document, this.operationName, this.variables, this.attributes));
new DefaultClientGraphQlRequest(document, this.operationName, this.variables, this.extensions, this.attributes));
}
}

View File

@@ -151,6 +151,21 @@ public interface GraphQlClient {
*/
RequestSpec variables(Map<String, Object> variables);
/**
* Add a value for a protocol extension.
* @param name the protocol extension name
* @param value the extension value
* @return this request spec
*/
RequestSpec extension(String name, @Nullable Object value);
/**
* Add all given protocol extensions.
* @param extensions the protocol extensions
* @return this request spec
*/
RequestSpec extensions(Map<String, Object> extensions);
/**
* Set a client request attribute.
* <p>This is purely for client side request processing, i.e. available

View File

@@ -43,7 +43,8 @@ public class RSocketGraphQlRequest extends DefaultExecutionGraphQlRequest implem
* @param locale the locale from the HTTP request, if any
*/
public RSocketGraphQlRequest(Map<String, Object> body, String id, @Nullable Locale locale) {
super(getKey("query", body), getKey("operationName", body), getKey("variables", body), id, locale);
super(getKey("query", body), getKey("operationName", body), getKey("variables", body),
getKey("extensions", body), id, locale);
}
@SuppressWarnings("unchecked")

View File

@@ -58,7 +58,8 @@ public class WebGraphQlRequest extends DefaultExecutionGraphQlRequest implements
public WebGraphQlRequest(
URI uri, HttpHeaders headers, Map<String, Object> body, String id, @Nullable Locale locale) {
super(getKey("query", body), getKey("operationName", body), getKey("variables", body), id, locale);
super(getKey("query", body), getKey("operationName", body), getKey("variables", body),
getKey("extensions", body), id, locale);
Assert.notNull(uri, "URI is required'");
Assert.notNull(headers, "HttpHeaders is required'");

View File

@@ -61,14 +61,16 @@ public class DefaultExecutionGraphQlRequest extends DefaultGraphQlRequest implem
* @param document textual representation of the operation(s)
* @param operationName optionally, the name of the operation to execute
* @param variables variables by which the query is parameterized
* @param extensions implementor specific, protocol extensions
* @param id the request id, to be used as the {@link ExecutionId}
* @param locale the locale associated with the request
*/
public DefaultExecutionGraphQlRequest(
String document, @Nullable String operationName, @Nullable Map<String, Object> variables,
String document, @Nullable String operationName,
@Nullable Map<String, Object> variables, @Nullable Map<String, Object> extensions,
String id, @Nullable Locale locale) {
super(document, operationName, variables);
super(document, operationName, variables, extensions);
Assert.notNull(id, "'id' is required");
this.id = id;
this.locale = locale;
@@ -109,6 +111,7 @@ public class DefaultExecutionGraphQlRequest extends DefaultGraphQlRequest implem
.query(getDocument())
.operationName(getOperationName())
.variables(getVariables())
.extensions(getExtensions())
.locale(this.locale)
.executionId(this.executionId != null ? this.executionId : ExecutionId.from(this.id));

View File

@@ -30,6 +30,7 @@ import org.springframework.util.ObjectUtils;
* Default implementation of {@link GraphQlRequest}.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 1.0.0
*/
public class DefaultGraphQlRequest implements GraphQlRequest {
@@ -41,13 +42,15 @@ public class DefaultGraphQlRequest implements GraphQlRequest {
private final Map<String, Object> variables;
private final Map<String, Object> extensions;
/**
* Create a request.
* @param document textual representation of the operation(s)
*/
public DefaultGraphQlRequest(String document) {
this(document, null, null);
this(document, null, null, null);
}
/**
@@ -55,14 +58,17 @@ public class DefaultGraphQlRequest implements GraphQlRequest {
* @param document textual representation of the operation(s)
* @param operationName optionally, the name of the operation to execute
* @param variables variables by which the operation is parameterized
* @param extensions implementor specific, protocol extensions
*/
public DefaultGraphQlRequest(
String document, @Nullable String operationName, @Nullable Map<String, Object> variables) {
String document, @Nullable String operationName,
@Nullable Map<String, Object> variables, @Nullable Map<String, Object> extensions) {
Assert.notNull(document, "'document' is required");
this.document = document;
this.operationName = operationName;
this.variables = (variables != null ? variables : Collections.emptyMap());
this.extensions = (extensions != null ? extensions : Collections.emptyMap());
}
@@ -82,6 +88,11 @@ public class DefaultGraphQlRequest implements GraphQlRequest {
return this.variables;
}
@Override
public Map<String, Object> getExtensions() {
return this.extensions;
}
@Override
public Map<String, Object> toMap() {
Map<String, Object> map = new LinkedHashMap<>(3);
@@ -92,6 +103,9 @@ public class DefaultGraphQlRequest implements GraphQlRequest {
if (!CollectionUtils.isEmpty(getVariables())) {
map.put("variables", new LinkedHashMap<>(getVariables()));
}
if (!CollectionUtils.isEmpty(getExtensions())) {
map.put("extensions", new LinkedHashMap<>(getExtensions()));
}
return map;
}
@@ -104,21 +118,24 @@ public class DefaultGraphQlRequest implements GraphQlRequest {
DefaultGraphQlRequest other = (DefaultGraphQlRequest) o;
return (getDocument().equals(other.getDocument()) &&
ObjectUtils.nullSafeEquals(getOperationName(), other.getOperationName()) &&
ObjectUtils.nullSafeEquals(getVariables(), other.getVariables()));
ObjectUtils.nullSafeEquals(getVariables(), other.getVariables()) &&
ObjectUtils.nullSafeEquals(getExtensions(), other.getExtensions()));
}
@Override
public int hashCode() {
return this.document.hashCode() +
31 * ObjectUtils.nullSafeHashCode(this.operationName) +
31 * this.variables.hashCode();
31 * this.variables.hashCode() +
31 * this.extensions.hashCode();
}
@Override
public String toString() {
return "document='" + getDocument() + "'" +
((getOperationName() != null) ? ", operationName='" + getOperationName() + "'" : "") +
(!CollectionUtils.isEmpty(getVariables()) ? ", variables=" + getVariables() : "");
(!CollectionUtils.isEmpty(getVariables()) ? ", variables=" + getVariables() : "" +
(!CollectionUtils.isEmpty(getExtensions()) ? ", extensions=" + getExtensions() : ""));
}
}

View File

@@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DefaultExecutionGraphQlRequestTests {
private final DefaultExecutionGraphQlRequest request =
new DefaultExecutionGraphQlRequest("greeting", "Greeting", null, "id", null);
new DefaultExecutionGraphQlRequest("greeting", "Greeting", null, null, "id", null);
@Test

View File

@@ -158,7 +158,7 @@ public class DefaultGraphQlClientResponseTests {
private ClientGraphQlResponse creatResponse(Map<String, Object> responseMap) {
return new DefaultClientGraphQlResponse(
new DefaultClientGraphQlRequest("{test}", null, Collections.emptyMap(), Collections.emptyMap()),
new DefaultClientGraphQlRequest("{test}", null, Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap()),
new ResponseMapGraphQlResponse(responseMap),
new Jackson2JsonEncoder(), new Jackson2JsonDecoder());
}

View File

@@ -101,7 +101,7 @@ public class GraphQlClientTests extends GraphQlClientTestSupport {
vars.put("foo", "bar");
vars.put("keyOnly", null);
GraphQlRequest request = new DefaultGraphQlRequest("mockRequest1", "HeroNameAndFriends", vars);
GraphQlRequest request = new DefaultGraphQlRequest("mockRequest1", "HeroNameAndFriends", vars, null);
getGraphQlService().setDataAsJson(request.getDocument(), "{\"hero\": {\"name\":\"R2-D2\"}}");
MovieCharacter character = graphQlClient().document(document)

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2020-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.support;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultGraphQlRequest}.
* @author Brian Clozel
*/
class DefaultGraphQlRequestTests {
@Test
void requestAsMapShouldContainAllEntries() {
String document = "query HeroNameAndFriends($episode: Episode) {" +
" hero(episode: $episode) {" +
" name"
+ " }" +
"}";
Map<String, Object> variables = Collections.singletonMap("episode", "JEDI");
Map<String, Object> extensions = Collections.singletonMap("myExtension", "value");
DefaultExecutionGraphQlRequest request = new DefaultExecutionGraphQlRequest(document, "HeroNameAndFriends",
variables, extensions, "1", null);
assertThat(request.toMap()).containsEntry("query", document).containsEntry("operationName", "HeroNameAndFriends")
.containsEntry("variables", variables).containsEntry("extensions", extensions);
}
}

View File

@@ -32,7 +32,7 @@ public class TestExecutionRequest extends DefaultExecutionGraphQlRequest {
private TestExecutionRequest(String document) {
super(document, null, null, String.valueOf(idIndex.incrementAndGet()), null);
super(document, null, null, null, String.valueOf(idIndex.incrementAndGet()), null);
}

View File

@@ -145,7 +145,7 @@ public class MockExecutionGraphQlService implements ExecutionGraphQlService {
private ExecutionGraphQlRequest toExecutionRequest(GraphQlRequest request) {
return new DefaultExecutionGraphQlRequest(
request.getDocument(), request.getOperationName(), request.getVariables(), "1", null);
request.getDocument(), request.getOperationName(), request.getVariables(), request.getExtensions(), "1", null);
}
}