Add client side interception

Closes gh-332
This commit is contained in:
rstoyanchev
2022-03-21 07:28:00 +00:00
parent bb4ebf6cb8
commit 92547de5d0
21 changed files with 482 additions and 107 deletions

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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<B extends AbstractGraphQlClie
"com.fasterxml.jackson.databind.ObjectMapper", AbstractGraphQlClientBuilder.class.getClassLoader());
private final List<GraphQlClientInterceptor> interceptors = new ArrayList<>();
private DocumentSource documentSource = new CachingDocumentSource(new ResourceDocumentSource());
@Nullable
@@ -67,6 +74,18 @@ public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClie
}
@Override
public B interceptor(GraphQlClientInterceptor... interceptors) {
this.interceptors.addAll(Arrays.asList(interceptors));
return self();
}
@Override
public B interceptors(Consumer<List<GraphQlClientInterceptor>> interceptorsConsumer) {
interceptorsConsumer.accept(this.interceptors);
return self();
}
@Override
public B documentSource(DocumentSource contentLoader) {
this.documentSource = contentLoader;
@@ -104,7 +123,7 @@ public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClie
}
return new DefaultGraphQlClient(
this.documentSource, transport, getJsonEncoder(), getJsonDecoder(), getBuilderInitializer());
this.documentSource, createExecuteChain(transport), createExecuteSubscriptionChain(transport));
}
/**
@@ -112,17 +131,40 @@ public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClie
*/
protected Consumer<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;
}

View File

@@ -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.
* <p>The attributes purely for client side request processing, i.e. available
* throughout the {@link GraphQlClientInterceptor} chain, but not sent.
*/
Map<String, Object> getAttributes();
}

View File

@@ -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}
*/

View File

@@ -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<String, Object> attributes = new ConcurrentHashMap<>();
DefaultClientGraphQlRequest(
String document, @Nullable String operationName, Map<String, Object> variables,
Map<String, Object> attributes) {
super(document, operationName, variables);
this.attributes.putAll(attributes);
}
@Override
public Map<String, Object> getAttributes() {
return this.attributes;
}
}

View File

@@ -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;
}

View File

@@ -107,7 +107,7 @@ final class DefaultClientResponseField implements ClientResponseField {
@SuppressWarnings({"unchecked", "ConstantConditions"})
private <T> 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;

View File

@@ -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<AbstractGraphQlClientBuilder<?>> builderInitializer;
private final GraphQlClientInterceptor.SubscriptionChain executeSubscriptionChain;
DefaultGraphQlClient(
DocumentSource documentSource, GraphQlTransport transport,
Encoder<?> jsonEncoder, Decoder<?> jsonDecoder,
Consumer<AbstractGraphQlClientBuilder<?>> 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<Builder> {
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<String, Object> variables = new LinkedHashMap<>();
private final Map<String, Object> attributes = new LinkedHashMap<>();
DefaultRequestSpec(Mono<String> 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<Map<String, Object>> 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<ClientGraphQlResponse> 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<ClientGraphQlResponse> 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<GraphQlRequest> initRequest() {
private Mono<ClientGraphQlRequest> 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 <T> Mono<T> 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);
}

View File

@@ -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;
}

View File

@@ -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<AbstractGraphQlClientBuilder<?>> builderInitializer;
GenericGraphQlClient(
GraphQlClient graphQlClient, GraphQlTransport transport,
Consumer<AbstractGraphQlClientBuilder<?>> 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<Builder> {
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());
}
}
}

View File

@@ -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<B extends Builder<B>> {
/**
* 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<List<GraphQlClientInterceptor>> 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<String, Object> variables);
/**
* Set a client request attribute.
* <p>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<Map<String, Object>> attributesConsumer);
/**
* Shortcut for {@link #execute()} with a single field path to decode from.
* @return a spec with decoding options

View File

@@ -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<ClientGraphQlResponse> 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<ClientGraphQlResponse> 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<ClientGraphQlResponse> intercept(ClientGraphQlRequest request, Chain chain) {
return GraphQlClientInterceptor.this.intercept(
request, nextRequest -> interceptor.intercept(nextRequest, chain));
}
@Override
public Flux<ClientGraphQlResponse> 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<ClientGraphQlResponse> 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<ClientGraphQlResponse> next(ClientGraphQlRequest request);
}
}

View File

@@ -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<String, Object> map) {
return new MapGraphQlResponse(map);
static GraphQlResponse createResponse(Map<String, Object> responseMap) {
return new ResponseMapGraphQlResponse(responseMap);
}
}

View File

@@ -60,7 +60,7 @@ final class HttpGraphQlTransport implements GraphQlTransport {
.bodyValue(request.toMap())
.retrieve()
.bodyToMono(MAP_TYPE)
.map(GraphQlTransport::wrapResponseMap);
.map(ResponseMapGraphQlResponse::new);
}
@Override

View File

@@ -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<String, Object> responseMap;
private final List<ResponseError> errors;
MapGraphQlResponse(Map<String, Object> responseMap) {
ResponseMapGraphQlResponse(Map<String, Object> 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

View File

@@ -476,7 +476,7 @@ final class WebSocketGraphQlTransport implements GraphQlTransport {
}
Map<String, Object> 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<Map<String, Object>> 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) {

View File

@@ -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<String, Object> 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());
}

View File

@@ -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<String, Object> 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<ClientGraphQlRequest> requestConsumer) {
return new GraphQlClientInterceptor() {
@Override
public Mono<ClientGraphQlResponse> intercept(ClientGraphQlRequest request, Chain chain) {
requestConsumer.accept(request);
return chain.next(request);
}
};
}
}

View File

@@ -96,7 +96,7 @@ public class GraphQlClientTestSupport {
Map<String, Object> responseMap = executionResult.toSpecification();
when(this.transport.execute(eq(request)))
.thenReturn(Mono.just(GraphQlTransport.wrapResponseMap(responseMap)));
.thenReturn(Mono.just(new ResponseMapGraphQlResponse(responseMap)));
}
@SuppressWarnings("unchecked")

View File

@@ -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")));