diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractDirectGraphQlTransport.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractDirectGraphQlTransport.java index 7672bfda..359587f1 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractDirectGraphQlTransport.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractDirectGraphQlTransport.java @@ -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); } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java index ace71e43..36157ce0 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java @@ -125,6 +125,8 @@ final class DefaultGraphQlTester implements GraphQlTester { private final Map variables = new LinkedHashMap<>(); + private final Map 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) { diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java index b0cc934e..b14c1d21 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java @@ -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. diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java index 7384ea17..4c171f03 100644 --- a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java @@ -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() { diff --git a/spring-graphql/src/main/java/org/springframework/graphql/GraphQlRequest.java b/spring-graphql/src/main/java/org/springframework/graphql/GraphQlRequest.java index a5938b82..84062e67 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/GraphQlRequest.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/GraphQlRequest.java @@ -53,6 +53,11 @@ public interface GraphQlRequest { */ Map getVariables(); + /** + * Return implementor specific, protocol extensions, if any. + */ + Map getExtensions(); + /** * Convert the request to a {@link Map} as defined in * GraphQL over HTTP and diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientGraphQlRequest.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientGraphQlRequest.java index 2a9d4d5a..ad576e45 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientGraphQlRequest.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientGraphQlRequest.java @@ -35,10 +35,11 @@ final class DefaultClientGraphQlRequest extends DefaultGraphQlRequest implements DefaultClientGraphQlRequest( - String document, @Nullable String operationName, Map variables, + String document, @Nullable String operationName, + Map variables, Map extensions, Map attributes) { - super(document, operationName, variables); + super(document, operationName, variables, extensions); this.attributes.putAll(attributes); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java index dbb7c328..929578c5 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java @@ -94,6 +94,8 @@ final class DefaultGraphQlClient implements GraphQlClient { private final Map attributes = new LinkedHashMap<>(); + private final Map extensions = new LinkedHashMap<>(); + DefaultRequestSpec(Mono 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 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 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)); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java index bf334390..42c6e49d 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java @@ -151,6 +151,21 @@ public interface GraphQlClient { */ RequestSpec variables(Map 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 extensions); + /** * Set a client request attribute. *

This is purely for client side request processing, i.e. available diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/RSocketGraphQlRequest.java b/spring-graphql/src/main/java/org/springframework/graphql/server/RSocketGraphQlRequest.java index 9f3034c2..17e89c1d 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/RSocketGraphQlRequest.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/RSocketGraphQlRequest.java @@ -43,7 +43,8 @@ public class RSocketGraphQlRequest extends DefaultExecutionGraphQlRequest implem * @param locale the locale from the HTTP request, if any */ public RSocketGraphQlRequest(Map 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") diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/WebGraphQlRequest.java b/spring-graphql/src/main/java/org/springframework/graphql/server/WebGraphQlRequest.java index e75aa457..d646a035 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/WebGraphQlRequest.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/WebGraphQlRequest.java @@ -58,7 +58,8 @@ public class WebGraphQlRequest extends DefaultExecutionGraphQlRequest implements public WebGraphQlRequest( URI uri, HttpHeaders headers, Map 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'"); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultExecutionGraphQlRequest.java b/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultExecutionGraphQlRequest.java index 227bed73..415701e1 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultExecutionGraphQlRequest.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultExecutionGraphQlRequest.java @@ -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 variables, + String document, @Nullable String operationName, + @Nullable Map variables, @Nullable Map 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)); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultGraphQlRequest.java b/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultGraphQlRequest.java index 14b3ef7e..000e6f2e 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultGraphQlRequest.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultGraphQlRequest.java @@ -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 variables; + private final Map 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 variables) { + String document, @Nullable String operationName, + @Nullable Map variables, @Nullable Map 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 getExtensions() { + return this.extensions; + } + @Override public Map toMap() { Map 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() : "")); } } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/DefaultExecutionGraphQlRequestTests.java b/spring-graphql/src/test/java/org/springframework/graphql/DefaultExecutionGraphQlRequestTests.java index 77f886f7..399f10a4 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/DefaultExecutionGraphQlRequestTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/DefaultExecutionGraphQlRequestTests.java @@ -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 diff --git a/spring-graphql/src/test/java/org/springframework/graphql/client/DefaultGraphQlClientResponseTests.java b/spring-graphql/src/test/java/org/springframework/graphql/client/DefaultGraphQlClientResponseTests.java index 8abbee4e..a198a527 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/client/DefaultGraphQlClientResponseTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/client/DefaultGraphQlClientResponseTests.java @@ -158,7 +158,7 @@ public class DefaultGraphQlClientResponseTests { private ClientGraphQlResponse creatResponse(Map 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()); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientTests.java b/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientTests.java index 712df860..8fc86da0 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientTests.java @@ -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) diff --git a/spring-graphql/src/test/java/org/springframework/graphql/support/DefaultGraphQlRequestTests.java b/spring-graphql/src/test/java/org/springframework/graphql/support/DefaultGraphQlRequestTests.java new file mode 100644 index 00000000..788dd935 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/support/DefaultGraphQlRequestTests.java @@ -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 variables = Collections.singletonMap("episode", "JEDI"); + Map 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); + } + +} \ No newline at end of file diff --git a/spring-graphql/src/testFixtures/java/org/springframework/graphql/TestExecutionRequest.java b/spring-graphql/src/testFixtures/java/org/springframework/graphql/TestExecutionRequest.java index 255e6dac..b4812ad6 100644 --- a/spring-graphql/src/testFixtures/java/org/springframework/graphql/TestExecutionRequest.java +++ b/spring-graphql/src/testFixtures/java/org/springframework/graphql/TestExecutionRequest.java @@ -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); } diff --git a/spring-graphql/src/testFixtures/java/org/springframework/graphql/execution/MockExecutionGraphQlService.java b/spring-graphql/src/testFixtures/java/org/springframework/graphql/execution/MockExecutionGraphQlService.java index 8bc42a7a..7126a54c 100644 --- a/spring-graphql/src/testFixtures/java/org/springframework/graphql/execution/MockExecutionGraphQlService.java +++ b/spring-graphql/src/testFixtures/java/org/springframework/graphql/execution/MockExecutionGraphQlService.java @@ -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); } }