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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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'");
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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() : ""));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user