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 188b31d1..7672bfda 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 @@ -28,9 +28,9 @@ import org.springframework.graphql.ExecutionGraphQlResponse; import org.springframework.graphql.GraphQlRequest; import org.springframework.graphql.GraphQlResponse; import org.springframework.graphql.ResponseError; +import org.springframework.graphql.client.GraphQlTransport; import org.springframework.graphql.support.DefaultExecutionGraphQlRequest; import org.springframework.graphql.support.DefaultExecutionGraphQlResponse; -import org.springframework.graphql.client.GraphQlTransport; import org.springframework.test.util.AssertionErrors; import org.springframework.util.AlternativeJdkIdGenerator; import org.springframework.util.CollectionUtils; diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebTestClientTransport.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebTestClientTransport.java index 03d7a4ad..90469759 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebTestClientTransport.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebTestClientTransport.java @@ -66,7 +66,7 @@ final class WebTestClientTransport implements GraphQlTransport { .getResponseBody(); responseMap = (responseMap != null ? responseMap : Collections.emptyMap()); - GraphQlResponse response = GraphQlTransport.wrapResponseMap(responseMap); + GraphQlResponse response = GraphQlTransport.createResponse(responseMap); return Mono.just(response); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java index ee5d4630..a0959cc2 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java @@ -16,10 +16,15 @@ package org.springframework.graphql.client; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.function.Consumer; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; +import org.springframework.graphql.client.GraphQlClientInterceptor.Chain; +import org.springframework.graphql.client.GraphQlClientInterceptor.SubscriptionChain; import org.springframework.graphql.support.CachingDocumentSource; import org.springframework.graphql.support.DocumentSource; import org.springframework.graphql.support.ResourceDocumentSource; @@ -49,6 +54,8 @@ public abstract class AbstractGraphQlClientBuilder interceptors = new ArrayList<>(); + private DocumentSource documentSource = new CachingDocumentSource(new ResourceDocumentSource()); @Nullable @@ -67,6 +74,18 @@ public abstract class AbstractGraphQlClientBuilder> interceptorsConsumer) { + interceptorsConsumer.accept(this.interceptors); + return self(); + } + @Override public B documentSource(DocumentSource contentLoader) { this.documentSource = contentLoader; @@ -104,7 +123,7 @@ public abstract class AbstractGraphQlClientBuilder> getBuilderInitializer() { return builder -> { + builder.interceptors(interceptorList -> interceptorList.addAll(interceptors)); builder.documentSource(documentSource); - builder.setJsonCodecs(getJsonEncoder(), getJsonDecoder()); + builder.setJsonCodecs(getEncoder(), getDecoder()); }; } - private Encoder getJsonEncoder() { + private Chain createExecuteChain(GraphQlTransport transport) { + + Chain chain = request -> transport.execute(request).map(response -> + new DefaultClientGraphQlResponse(request, response, getEncoder(), getDecoder())); + + return this.interceptors.stream() + .reduce(GraphQlClientInterceptor::andThen) + .map(interceptor -> (Chain) (request) -> interceptor.intercept(request, chain)) + .orElse(chain); + } + + private SubscriptionChain createExecuteSubscriptionChain(GraphQlTransport transport) { + + SubscriptionChain chain = request -> transport.executeSubscription(request) + .map(response -> new DefaultClientGraphQlResponse(request, response, getEncoder(), getDecoder())); + + return this.interceptors.stream() + .reduce(GraphQlClientInterceptor::andThen) + .map(interceptor -> (SubscriptionChain) (request) -> interceptor.interceptSubscription(request, chain)) + .orElse(chain); + } + + private Encoder getEncoder() { Assert.notNull(this.jsonEncoder, "jsonEncoder has not been set"); return this.jsonEncoder; } - private Decoder getJsonDecoder() { + private Decoder getDecoder() { Assert.notNull(this.jsonDecoder, "jsonDecoder has not been set"); return this.jsonDecoder; } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlRequest.java b/spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlRequest.java new file mode 100644 index 00000000..66f5e982 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlRequest.java @@ -0,0 +1,40 @@ +/* + * 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.client; + +import java.util.Map; + +import org.springframework.graphql.GraphQlRequest; + + +/** + * {@link GraphQlRequest} for client side use. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public interface ClientGraphQlRequest extends GraphQlRequest { + + /** + * Return the client request attributes. + *

The attributes purely for client side request processing, i.e. available + * throughout the {@link GraphQlClientInterceptor} chain, but not sent. + */ + Map getAttributes(); + +} + diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlResponse.java b/spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlResponse.java index b986f260..a43c29c2 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlResponse.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlResponse.java @@ -18,7 +18,6 @@ package org.springframework.graphql.client; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.graphql.GraphQlRequest; import org.springframework.graphql.GraphQlResponse; /** @@ -30,11 +29,6 @@ import org.springframework.graphql.GraphQlResponse; */ public interface ClientGraphQlResponse extends GraphQlResponse { - /** - * Return the request for the response. - */ - GraphQlRequest getRequest(); - /** * {@inheritDoc} */ 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 new file mode 100644 index 00000000..2a9d4d5a --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientGraphQlRequest.java @@ -0,0 +1,51 @@ +/* + * 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.client; + + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.graphql.support.DefaultGraphQlRequest; +import org.springframework.lang.Nullable; + +/** + * Default implementation of {@link ClientGraphQlRequest}. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +final class DefaultClientGraphQlRequest extends DefaultGraphQlRequest implements ClientGraphQlRequest { + + private final Map attributes = new ConcurrentHashMap<>(); + + + DefaultClientGraphQlRequest( + String document, @Nullable String operationName, Map variables, + Map attributes) { + + super(document, operationName, variables); + this.attributes.putAll(attributes); + } + + + @Override + public Map getAttributes() { + return this.attributes; + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientGraphQlResponse.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientGraphQlResponse.java index 76807ae4..5450f971 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientGraphQlResponse.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientGraphQlResponse.java @@ -19,7 +19,6 @@ package org.springframework.graphql.client; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; -import org.springframework.graphql.GraphQlRequest; import org.springframework.graphql.GraphQlResponse; @@ -29,9 +28,9 @@ import org.springframework.graphql.GraphQlResponse; * @author Rossen Stoyanchev * @since 1.0.0 */ -final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements ClientGraphQlResponse { +final class DefaultClientGraphQlResponse extends ResponseMapGraphQlResponse implements ClientGraphQlResponse { - private final GraphQlRequest request; + private final ClientGraphQlRequest request; private final Encoder encoder; @@ -39,7 +38,7 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C DefaultClientGraphQlResponse( - GraphQlRequest request, GraphQlResponse response, Encoder encoder, Decoder decoder) { + ClientGraphQlRequest request, GraphQlResponse response, Encoder encoder, Decoder decoder) { super(response); @@ -49,8 +48,7 @@ final class DefaultClientGraphQlResponse extends MapGraphQlResponse implements C } - @Override - public GraphQlRequest getRequest() { + ClientGraphQlRequest getRequest() { return this.request; } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientResponseField.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientResponseField.java index 337417f2..20ed2a06 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientResponseField.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientResponseField.java @@ -107,7 +107,7 @@ final class DefaultClientResponseField implements ClientResponseField { @SuppressWarnings({"unchecked", "ConstantConditions"}) private T toEntity(ResolvableType targetType) { if (!hasValue()) { - throw new FieldAccessException(this.response, this); + throw new FieldAccessException(this.response.getRequest(), this.response, this); } DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; 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 6a5a9db9..c501346a 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 @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.graphql.client; import java.util.Collections; @@ -25,11 +26,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.codec.Decoder; -import org.springframework.core.codec.Encoder; -import org.springframework.graphql.support.DefaultGraphQlRequest; -import org.springframework.graphql.GraphQlRequest; -import org.springframework.graphql.GraphQlResponse; import org.springframework.graphql.support.DocumentSource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -44,31 +40,22 @@ final class DefaultGraphQlClient implements GraphQlClient { private final DocumentSource documentSource; - private final GraphQlTransport transport; + private final GraphQlClientInterceptor.Chain executeChain; - private final Encoder jsonEncoder; - - private final Decoder jsonDecoder; - - private final Consumer> builderInitializer; + private final GraphQlClientInterceptor.SubscriptionChain executeSubscriptionChain; DefaultGraphQlClient( - DocumentSource documentSource, GraphQlTransport transport, - Encoder jsonEncoder, Decoder jsonDecoder, - Consumer> builderInitializer) { + DocumentSource documentSource, GraphQlClientInterceptor.Chain executeChain, + GraphQlClientInterceptor.SubscriptionChain executeSubscriptionChain) { Assert.notNull(documentSource, "DocumentSource is required"); - Assert.notNull(transport, "GraphQlTransport is required"); - Assert.notNull(jsonEncoder, "'jsonEncoder' is required"); - Assert.notNull(jsonEncoder, "'jsonDecoder' is required"); - Assert.notNull(builderInitializer, "`builderInitializer` is required"); + Assert.notNull(executeChain, "GraphQlClientInterceptor.Chain is required"); + Assert.notNull(executeSubscriptionChain, "GraphQlClientInterceptor.SubscriptionChain is required"); this.documentSource = documentSource; - this.transport = transport; - this.jsonEncoder = jsonEncoder; - this.jsonDecoder = jsonDecoder; - this.builderInitializer = builderInitializer; + this.executeChain = executeChain; + this.executeSubscriptionChain = executeSubscriptionChain; } @@ -82,31 +69,14 @@ final class DefaultGraphQlClient implements GraphQlClient { return new DefaultRequestSpec(this.documentSource.getDocument(name)); } - @Override - public Builder mutate() { - Builder builder = new Builder(this.transport); - this.builderInitializer.accept(builder); - return builder; - } - - /** - * Default {@link GraphQlClient.Builder} with a given transport. + * The default client is unaware of transport details, and doesn't implement + * this method. It should always be wrapped via with a transport specific + * {@link AbstractDelegatingGraphQlClient} that implements mutation. */ - static final class Builder extends AbstractGraphQlClientBuilder { - - private final GraphQlTransport transport; - - Builder(GraphQlTransport transport) { - Assert.notNull(transport, "GraphQlTransport is required"); - this.transport = transport; - } - - @Override - public GraphQlClient build() { - return super.buildGraphQlClient(this.transport); - } - + @Override + public Builder mutate() { + throw new UnsupportedOperationException(); } @@ -122,6 +92,8 @@ final class DefaultGraphQlClient implements GraphQlClient { private final Map variables = new LinkedHashMap<>(); + private final Map attributes = new LinkedHashMap<>(); + DefaultRequestSpec(Mono documentMono) { Assert.notNull(documentMono, "'document' is required"); this.documentMono = documentMono; @@ -145,6 +117,18 @@ final class DefaultGraphQlClient implements GraphQlClient { return this; } + @Override + public RequestSpec attribute(String name, Object value) { + this.attributes.put(name, value); + return this; + } + + @Override + public RequestSpec attributes(Consumer> attributesConsumer) { + attributesConsumer.accept(this.attributes); + return this; + } + @Override public RetrieveSpec retrieve(String path) { return new DefaultRetrieveSpec(execute(), path); @@ -157,35 +141,23 @@ final class DefaultGraphQlClient implements GraphQlClient { @Override public Mono execute() { - return initRequest().flatMap(request -> - transport.execute(request) - .map(response -> initResponse(request, response)) - .onErrorResume( - ex -> !(ex instanceof GraphQlClientException), - ex -> toGraphQlTransportException(ex, request))); + return initRequest().flatMap(request -> executeChain.next(request) + .onErrorResume( + ex -> !(ex instanceof GraphQlClientException), + ex -> Mono.error(new GraphQlTransportException(ex, request)))); } @Override public Flux executeSubscription() { - return initRequest().flatMapMany(request -> - transport.executeSubscription(request) - .map(response -> initResponse(request, response)) - .onErrorResume( - ex -> !(ex instanceof GraphQlClientException), - ex -> toGraphQlTransportException(ex, request))); + return initRequest().flatMapMany(request -> executeSubscriptionChain.next(request) + .onErrorResume( + ex -> !(ex instanceof GraphQlClientException), + ex -> Mono.error(new GraphQlTransportException(ex, request)))); } - private Mono initRequest() { + private Mono initRequest() { return this.documentMono.map(document -> - new DefaultGraphQlRequest(document, this.operationName, this.variables)); - } - - private DefaultClientGraphQlResponse initResponse(GraphQlRequest request, GraphQlResponse response) { - return new DefaultClientGraphQlResponse(request, response, jsonEncoder, jsonDecoder); - } - - private Mono toGraphQlTransportException(Throwable ex, GraphQlRequest request) { - return Mono.error(new GraphQlTransportException(ex, request)); + new DefaultClientGraphQlRequest(document, this.operationName, this.variables, this.attributes)); } } @@ -207,7 +179,8 @@ final class DefaultGraphQlClient implements GraphQlClient { protected ClientResponseField getValidField(ClientGraphQlResponse response) { ClientResponseField field = response.field(this.path); if (!response.isValid() || field.getError() != null) { - throw new FieldAccessException(response, field); + throw new FieldAccessException( + ((DefaultClientGraphQlResponse) response).getRequest(), response, field); } return (field.hasValue() ? field : null); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/FieldAccessException.java b/spring-graphql/src/main/java/org/springframework/graphql/client/FieldAccessException.java index a074c309..da5eb8f1 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/FieldAccessException.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/FieldAccessException.java @@ -40,8 +40,10 @@ public class FieldAccessException extends GraphQlClientException { /** * Constructor with the request and response, and the accessed field. */ - public FieldAccessException(ClientGraphQlResponse response, ClientResponseField field) { - super(initDefaultMessage(field), null, response.getRequest()); + public FieldAccessException( + ClientGraphQlRequest request, ClientGraphQlResponse response, ClientResponseField field) { + + super(initDefaultMessage(field), null, request); this.response = response; this.field = field; } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/GenericGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/GenericGraphQlClient.java new file mode 100644 index 00000000..70f6a945 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/GenericGraphQlClient.java @@ -0,0 +1,78 @@ +/* + * 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.function.Consumer; + +import org.springframework.util.Assert; + + +/** + * GraphQL client with a given, externally prepared transport. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +final class GenericGraphQlClient extends AbstractDelegatingGraphQlClient { + + private final GraphQlTransport transport; + + private final Consumer> builderInitializer; + + + GenericGraphQlClient( + GraphQlClient graphQlClient, GraphQlTransport transport, + Consumer> builderInitializer) { + + super(graphQlClient); + Assert.notNull(transport, "GraphQlTransport is required"); + Assert.notNull(builderInitializer, "'builderInitializer' is required"); + this.transport = transport; + this.builderInitializer = builderInitializer; + } + + + @Override + public Builder mutate() { + Builder builder = new Builder(transport); + this.builderInitializer.accept(builder); + return builder; + } + + + /** + * Default {@link GraphQlClient.Builder} with a given transport. + */ + static final class Builder extends AbstractGraphQlClientBuilder { + + private final GraphQlTransport transport; + + Builder(GraphQlTransport transport) { + Assert.notNull(transport, "GraphQlTransport is required"); + this.transport = transport; + } + + @Override + public GraphQlClient build() { + GraphQlClient client = buildGraphQlClient(this.transport); + return new GenericGraphQlClient(client, this.transport, getBuilderInitializer()); + } + + } + +} 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 96026737..242c22b8 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 @@ -17,6 +17,7 @@ package org.springframework.graphql.client; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -80,7 +81,7 @@ public interface GraphQlClient { * @return the builder for further initialization */ static Builder builder(GraphQlTransport transport) { - return new DefaultGraphQlClient.Builder(transport); + return new GenericGraphQlClient.Builder(transport); } @@ -89,6 +90,22 @@ public interface GraphQlClient { */ interface Builder> { + /** + * Configure interceptors to be invoked before delegating to the + * {@link GraphQlTransport} to perform the request. + * @param interceptors the interceptors to add + * @return this builder + */ + B interceptor(GraphQlClientInterceptor... interceptors); + + /** + * Customize the list of interceptors. The provided list is "live", so + * the consumer can inspect and insert interceptors accordingly. + * @param interceptorsConsumer consumer to customize the interceptors with + * @return this builder + */ + B interceptors(Consumer> interceptorsConsumer); + /** * Configure a {@link DocumentSource} for use with * {@link #documentName(String)} for resolving a document by name. @@ -132,6 +149,24 @@ public interface GraphQlClient { */ RequestSpec variables(Map variables); + /** + * Set a client request attribute. + *

This is purely for client side request processing, i.e. available + * throughout the {@link GraphQlClientInterceptor} chain but not sent. + * @param name the name of the attribute + * @param value the attribute value + * @return this builder + */ + RequestSpec attribute(String name, Object value); + + /** + * Manipulate the client request attributes. The map provided to the consumer + * is "live", so the consumer can inspect and modify attributes accordingly. + * @param attributesConsumer consumer to customize attributes with + * @return this builder + */ + RequestSpec attributes(Consumer> attributesConsumer); + /** * Shortcut for {@link #execute()} with a single field path to decode from. * @return a spec with decoding options diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClientInterceptor.java b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClientInterceptor.java new file mode 100644 index 00000000..eb1f52ea --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClientInterceptor.java @@ -0,0 +1,111 @@ +/* + * 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.client; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + + +/** + * Interceptor for {@link GraphQlClient} requests. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public interface GraphQlClientInterceptor { + + /** + * Intercept a single response request (query and mutation operations) and + * delegate to the rest of the chain including other interceptors followed + * by the {@link GraphQlTransport}. + * @param request the request to perform + * @param chain the rest of the chain to perform the request + * @return a {@link Mono} for the response + * @see GraphQlClient.RequestSpec#execute() + */ + default Mono intercept(ClientGraphQlRequest request, Chain chain) { + return chain.next(request); + } + + /** + * Intercept a subscription request and delegate to the rest of the chain + * including other interceptors followed by the {@link GraphQlTransport}. + * @param request the request to perform + * @param chain the rest of the chain to perform the request + * @return a {@link Flux} with responses + * @see GraphQlClient.RequestSpec#executeSubscription() + */ + default Flux interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) { + return chain.next(request); + } + + /** + * Return a new {@link GraphQlClientInterceptor} that invokes the current + * interceptor first and then the one that is passed in. + * @param interceptor the interceptor to delegate to after "this" + * @return the new {@code GraphQlClientInterceptor} + */ + default GraphQlClientInterceptor andThen(GraphQlClientInterceptor interceptor) { + return new GraphQlClientInterceptor() { + + @Override + public Mono intercept(ClientGraphQlRequest request, Chain chain) { + return GraphQlClientInterceptor.this.intercept( + request, nextRequest -> interceptor.intercept(nextRequest, chain)); + } + + @Override + public Flux interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) { + return GraphQlClientInterceptor.this.interceptSubscription( + request, nextRequest -> interceptor.interceptSubscription(nextRequest, chain)); + } + }; + } + + + /** + * Contract for delegation of single response requests to the rest of the chain. + */ + interface Chain { + + /** + * Delegate to the rest of the chain to perform the request. + * @param request the request to perform. + * @return {@code Mono} with the response + * @see GraphQlClient.RequestSpec#execute() + */ + Mono next(ClientGraphQlRequest request); + + } + + + /** + * Contract for delegation of subscription requests to the rest of the chain. + */ + interface SubscriptionChain { + + /** + * Delegate to the rest of the chain to perform the request. + * @param request the request to perform + * @return {@code Flux} with responses + * @see GraphQlClient.RequestSpec#executeSubscription() + */ + Flux next(ClientGraphQlRequest request); + + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlTransport.java b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlTransport.java index 878b7a0a..2beee9c0 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlTransport.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlTransport.java @@ -61,10 +61,11 @@ public interface GraphQlTransport { /** - * Wrap the given response map and expose it as a {@link GraphQlResponse}. + * Factory method to create {@link GraphQlResponse} from a GraphQL response + * map for use in transport implementations. */ - static GraphQlResponse wrapResponseMap(Map map) { - return new MapGraphQlResponse(map); + static GraphQlResponse createResponse(Map responseMap) { + return new ResponseMapGraphQlResponse(responseMap); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java b/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java index f4f3e11b..65aa84ca 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java @@ -60,7 +60,7 @@ final class HttpGraphQlTransport implements GraphQlTransport { .bodyValue(request.toMap()) .retrieve() .bodyToMono(MAP_TYPE) - .map(GraphQlTransport::wrapResponseMap); + .map(ResponseMapGraphQlResponse::new); } @Override diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/MapGraphQlResponse.java b/spring-graphql/src/main/java/org/springframework/graphql/client/ResponseMapGraphQlResponse.java similarity index 94% rename from spring-graphql/src/main/java/org/springframework/graphql/client/MapGraphQlResponse.java rename to spring-graphql/src/main/java/org/springframework/graphql/client/ResponseMapGraphQlResponse.java index f04e80ad..5a1a1fbb 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/MapGraphQlResponse.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/ResponseMapGraphQlResponse.java @@ -38,20 +38,20 @@ import org.springframework.util.ObjectUtils; * @author Rossen Stoyanchev * @since 1.0.0 */ -class MapGraphQlResponse extends AbstractGraphQlResponse implements GraphQlResponse { +class ResponseMapGraphQlResponse extends AbstractGraphQlResponse { private final Map responseMap; private final List errors; - MapGraphQlResponse(Map responseMap) { + ResponseMapGraphQlResponse(Map responseMap) { Assert.notNull(responseMap, "'responseMap' is required"); this.responseMap = responseMap; this.errors = wrapErrors(responseMap); } - MapGraphQlResponse(GraphQlResponse response) { + protected ResponseMapGraphQlResponse(GraphQlResponse response) { Assert.notNull(response, "'GraphQlResponse' is required"); this.responseMap = response.toMap(); this.errors = response.getErrors(); @@ -94,8 +94,8 @@ class MapGraphQlResponse extends AbstractGraphQlResponse implements GraphQlRespo @Override public boolean equals(Object other) { - return (other instanceof MapGraphQlResponse && - this.responseMap.equals(((MapGraphQlResponse) other).responseMap)); + return (other instanceof ResponseMapGraphQlResponse && + this.responseMap.equals(((ResponseMapGraphQlResponse) other).responseMap)); } @Override diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java index 384ddd94..b5ffa5d5 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java @@ -476,7 +476,7 @@ final class WebSocketGraphQlTransport implements GraphQlTransport { } Map responseMap = message.getPayload(); - GraphQlResponse graphQlResponse = GraphQlTransport.wrapResponseMap(responseMap); + GraphQlResponse graphQlResponse = new ResponseMapGraphQlResponse(responseMap); Sinks.EmitResult emitResult = (responseState != null ? responseState.sink().tryEmitValue(graphQlResponse) : @@ -507,7 +507,7 @@ final class WebSocketGraphQlTransport implements GraphQlTransport { } List> errorList = message.getPayload(); - GraphQlResponse response = GraphQlTransport.wrapResponseMap(Collections.singletonMap("errors", errorList)); + GraphQlResponse response = new ResponseMapGraphQlResponse(Collections.singletonMap("errors", errorList)); Sinks.EmitResult emitResult; if (responseState != null) { 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 9e9f7c35..b0286329 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 @@ -28,7 +28,6 @@ import graphql.execution.ResultPath; import org.junit.jupiter.api.Test; import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.graphql.support.DefaultGraphQlRequest; import org.springframework.graphql.ResponseError; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; @@ -39,7 +38,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException /** - * Unit tests for {@link MapGraphQlResponse}. + * Unit tests for {@link DefaultClientGraphQlResponse}. * @author Rossen Stoyanchev */ public class DefaultGraphQlClientResponseTests { @@ -158,7 +157,8 @@ public class DefaultGraphQlClientResponseTests { private ClientGraphQlResponse creatResponse(Map responseMap) { return new DefaultClientGraphQlResponse( - new DefaultGraphQlRequest("{test}"), GraphQlTransport.wrapResponseMap(responseMap), + new DefaultClientGraphQlRequest("{test}", null, Collections.emptyMap(), Collections.emptyMap()), + new ResponseMapGraphQlResponse(responseMap), new Jackson2JsonEncoder(), new Jackson2JsonDecoder()); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientBuilderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientBuilderTests.java index a8365984..5436b522 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientBuilderTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientBuilderTests.java @@ -16,9 +16,14 @@ package org.springframework.graphql.client; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; +import org.springframework.graphql.GraphQlResponse; import org.springframework.graphql.support.DocumentSource; import static org.assertj.core.api.Assertions.assertThat; @@ -57,4 +62,49 @@ public class GraphQlClientBuilderTests extends GraphQlClientTestSupport { assertThat(response.isValid()).isTrue(); } + @Test + void mutateInterceptors() { + + String name = "name1"; + String value = "value1"; + + Map savedAttributes = new HashMap<>(); + + GraphQlClientInterceptor savingInterceptor = + initInterceptor(request -> savedAttributes.putAll(request.getAttributes())); + + GraphQlClientInterceptor changingInterceptor = + initInterceptor(request -> request.getAttributes().computeIfPresent(name, (k, v) -> v + "2")); + + initDataResponse(DOCUMENT, "{}"); + + // Original + GraphQlClient.Builder builder = graphQlClientBuilder().interceptor(savingInterceptor); + GraphQlClient client = builder.build(); + GraphQlResponse response = client.document(DOCUMENT).attribute(name, value).execute().block(TIMEOUT); + + assertThat(response).isNotNull(); + assertThat(response.isValid()).isTrue(); + assertThat(savedAttributes).hasSize(1).containsEntry(name, value); + + // Mutate + savedAttributes.clear(); + client = client.mutate().interceptors(interceptors -> interceptors.add(0, changingInterceptor)).build(); + response = client.document(DOCUMENT).attribute(name, value).execute().block(TIMEOUT); + + assertThat(response).isNotNull(); + assertThat(response.isValid()).isTrue(); + assertThat(savedAttributes).hasSize(1).containsEntry(name, value + "2"); + } + + private static GraphQlClientInterceptor initInterceptor(Consumer requestConsumer) { + return new GraphQlClientInterceptor() { + @Override + public Mono intercept(ClientGraphQlRequest request, Chain chain) { + requestConsumer.accept(request); + return chain.next(request); + } + }; + } + } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientTestSupport.java b/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientTestSupport.java index c716ea35..e57be3ab 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientTestSupport.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientTestSupport.java @@ -96,7 +96,7 @@ public class GraphQlClientTestSupport { Map responseMap = executionResult.toSpecification(); when(this.transport.execute(eq(request))) - .thenReturn(Mono.just(GraphQlTransport.wrapResponseMap(responseMap))); + .thenReturn(Mono.just(new ResponseMapGraphQlResponse(responseMap))); } @SuppressWarnings("unchecked") diff --git a/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java b/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java index fd858e91..4bf113d4 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java @@ -70,10 +70,10 @@ public class MockWebSocketGraphQlTransportTests { private final WebSocketGraphQlTransport transport = createTransport(this.webSocketClient); - private final GraphQlResponse response1 = GraphQlTransport.wrapResponseMap( + private final GraphQlResponse response1 = new ResponseMapGraphQlResponse( Collections.singletonMap("data", Collections.singletonMap("key1", "value1"))); - private final GraphQlResponse response2 = GraphQlTransport.wrapResponseMap( + private final GraphQlResponse response2 = new ResponseMapGraphQlResponse( Collections.singletonMap("data", Collections.singletonMap("key2", "value2")));