Add client side interception
Closes gh-332
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ final class HttpGraphQlTransport implements GraphQlTransport {
|
||||
.bodyValue(request.toMap())
|
||||
.retrieve()
|
||||
.bodyToMono(MAP_TYPE)
|
||||
.map(GraphQlTransport::wrapResponseMap);
|
||||
.map(ResponseMapGraphQlResponse::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")));
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user