Adapt CodecConfigurer to JSONPath MappingProvider

The JSON Encoder and Decoder implementations configured via
CodecConfigurer for HTTP and WebSocket are adapted to a JSONPath
MappingProvider and now automatically registered in JSONPath
configuration.

See gh-10, see gh-317
This commit is contained in:
rstoyanchev
2022-03-07 07:10:03 +00:00
parent abb51ee5e2
commit 1559c5989e
20 changed files with 493 additions and 148 deletions

View File

@@ -17,13 +17,15 @@ package org.springframework.graphql.test.tester;
import java.time.Duration;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import graphql.GraphQLError;
import org.springframework.graphql.client.AbstractGraphQlClientBuilder;
import org.springframework.graphql.client.GraphQlTransport;
import org.springframework.graphql.support.CachingDocumentSource;
import org.springframework.graphql.support.DocumentSource;
@@ -47,13 +49,8 @@ import org.springframework.util.ClassUtils;
*/
public abstract class AbstractGraphQlTesterBuilder<B extends AbstractGraphQlTesterBuilder<B>> implements GraphQlTester.Builder<B> {
private static final boolean jackson2Present;
static {
ClassLoader classLoader = AbstractGraphQlTesterBuilder.class.getClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
}
private static final boolean jackson2Present = ClassUtils.isPresent(
"com.fasterxml.jackson.databind.ObjectMapper", AbstractGraphQlClientBuilder.class.getClassLoader());
private static final Duration DEFAULT_RESPONSE_DURATION = Duration.ofSeconds(5);
@@ -63,6 +60,8 @@ public abstract class AbstractGraphQlTesterBuilder<B extends AbstractGraphQlTest
private DocumentSource documentSource = new CachingDocumentSource(new ResourceDocumentSource());
private Configuration jsonPathConfig = Configuration.builder().build();
private Duration responseTimeout = DEFAULT_RESPONSE_DURATION;
@@ -90,45 +89,57 @@ public abstract class AbstractGraphQlTesterBuilder<B extends AbstractGraphQlTest
return (T) this;
}
// Protected methods for use from build() in subclasses
/**
* Subclasses call this from {@link #build()} to provide the transport and get
* the default {@code GraphQlTester} to delegate to for request execution.
* Allow transport-specific subclass builders to register a JSON Path
* {@link MappingProvider} that matches the JSON encoding/decoding they use.
*/
protected GraphQlTester buildGraphQlTester(GraphQlTransport transport) {
Assert.notNull(transport, "GraphQlTransport is required");
return new DefaultGraphQlTester(
transport, this.errorFilter, initJsonPathConfig(), this.documentSource, this.responseTimeout,
getBuilderInitializer());
protected void configureJsonPathConfig(Function<Configuration, Configuration> configurer) {
this.jsonPathConfig = configurer.apply(this.jsonPathConfig);
}
private Configuration initJsonPathConfig() {
// Allow configuring JSONPath with codecs from transport subclasses
return (jackson2Present ? Jackson2Configuration.create() : Configuration.builder().build());
/**
* Build the default transport-agnostic client that subclasses can then wrap
* with {@link AbstractDelegatingGraphQlTester}.
*/
protected GraphQlTester buildGraphQlTester(GraphQlTransport transport) {
if (jackson2Present) {
configureJsonPathConfig(Jackson2Configurer::configure);
}
return new DefaultGraphQlTester(transport, this.errorFilter,
this.jsonPathConfig, this.documentSource, this.responseTimeout, getBuilderInitializer());
}
/**
* Subclasses call this from {@link #build()} to obtain a {@code Consumer} to
* initialize new builder instances with, based on "this" builder.
*/
protected Consumer<GraphQlTester.Builder<?>> getBuilderInitializer() {
protected Consumer<AbstractGraphQlTesterBuilder<?>> getBuilderInitializer() {
return builder -> {
if (this.errorFilter != null) {
builder.errorFilter(this.errorFilter);
}
builder.documentSource(this.documentSource);
builder.configureJsonPathConfig(config -> this.jsonPathConfig);
builder.responseTimeout(this.responseTimeout);
};
}
private static class Jackson2Configuration {
private static class Jackson2Configurer {
static Configuration create() {
return Configuration.builder()
.jsonProvider(new JacksonJsonProvider())
.mappingProvider(new JacksonMappingProvider())
.build();
private static final MappingProvider defaultProvider = Configuration.defaultConfiguration().mappingProvider();
static Configuration configure(Configuration config) {
return (config.mappingProvider() != null && config.mappingProvider() != defaultProvider ? config :
config.mappingProvider(new JacksonMappingProvider()));
}
}
}

View File

@@ -34,11 +34,11 @@ final class DefaultGraphQlServiceTester extends AbstractDelegatingGraphQlTester
private final GraphQlServiceTransport transport;
private final Consumer<GraphQlTester.Builder<?>> builderInitializer;
private final Consumer<AbstractGraphQlTesterBuilder<?>> builderInitializer;
DefaultGraphQlServiceTester(GraphQlTester tester, GraphQlServiceTransport transport,
Consumer<GraphQlTester.Builder<?>> builderInitializer) {
Consumer<AbstractGraphQlTesterBuilder<?>> builderInitializer) {
super(tester);

View File

@@ -69,7 +69,7 @@ final class DefaultGraphQlTester implements GraphQlTester {
private final Duration responseTimeout;
private final Consumer<GraphQlTester.Builder<?>> builderInitializer;
private final Consumer<AbstractGraphQlTesterBuilder<?>> builderInitializer;
/**
@@ -78,7 +78,12 @@ final class DefaultGraphQlTester implements GraphQlTester {
DefaultGraphQlTester(
GraphQlTransport transport, @Nullable Predicate<GraphQLError> errorFilter,
Configuration jsonPathConfig, DocumentSource documentSource, Duration timeout,
Consumer<GraphQlTester.Builder<?>> builderInitializer) {
Consumer<AbstractGraphQlTesterBuilder<?>> builderInitializer) {
Assert.notNull(transport, "GraphQlTransport is required");
Assert.notNull(jsonPathConfig, "JSONPath Configuration is required");
Assert.notNull(documentSource, "DocumentSource is required");
Assert.notNull(builderInitializer, "`builderInitializer` is required");
this.transport = transport;
this.errorFilter = errorFilter;

View File

@@ -20,6 +20,7 @@ package org.springframework.graphql.test.tester;
import java.net.URI;
import java.util.function.Consumer;
import org.springframework.graphql.client.CodecMappingProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.test.web.reactive.server.WebTestClient;
@@ -38,11 +39,11 @@ final class DefaultHttpGraphQlTester extends AbstractDelegatingGraphQlTester imp
private final WebTestClient webTestClient;
private final Consumer<GraphQlTester.Builder<?>> builderInitializer;
private final Consumer<AbstractGraphQlTesterBuilder<?>> builderInitializer;
DefaultHttpGraphQlTester(GraphQlTester graphQlTester, WebTestClient webTestClient,
Consumer<GraphQlTester.Builder<?>> builderInitializer) {
Consumer<AbstractGraphQlTesterBuilder<?>> builderInitializer) {
super(graphQlTester);
this.webTestClient = webTestClient;
@@ -109,11 +110,20 @@ final class DefaultHttpGraphQlTester extends AbstractDelegatingGraphQlTester imp
@Override
public HttpGraphQlTester build() {
registerJsonPathMappingProvider();
WebTestClient client = this.webTestClientBuilder.build();
GraphQlTester tester = super.buildGraphQlTester(new WebTestClientTransport(client));
return new DefaultHttpGraphQlTester(tester, client, getBuilderInitializer());
}
private void registerJsonPathMappingProvider() {
this.webTestClientBuilder.codecs(codecConfigurer ->
configureJsonPathConfig(config -> {
CodecMappingProvider provider = new CodecMappingProvider(codecConfigurer);
return config.mappingProvider(provider);
}));
}
}
}

View File

@@ -21,8 +21,10 @@ import java.net.URI;
import java.util.Arrays;
import java.util.function.Consumer;
import org.springframework.graphql.client.CodecMappingProvider;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.util.Assert;
import org.springframework.web.util.DefaultUriBuilderFactory;
@@ -38,11 +40,11 @@ final class DefaultWebGraphQlTester extends AbstractDelegatingGraphQlTester impl
private final WebGraphQlHandlerTransport transport;
private final Consumer<GraphQlTester.Builder<?>> builderInitializer;
private final Consumer<AbstractGraphQlTesterBuilder<?>> builderInitializer;
DefaultWebGraphQlTester(GraphQlTester tester, WebGraphQlHandlerTransport transport,
Consumer<GraphQlTester.Builder<?>> builderInitializer) {
Consumer<AbstractGraphQlTesterBuilder<?>> builderInitializer) {
super(tester);
this.transport = transport;
@@ -52,9 +54,7 @@ final class DefaultWebGraphQlTester extends AbstractDelegatingGraphQlTester impl
@Override
public Builder<?> mutate() {
Builder<?> builder = new Builder<>(this.transport.getGraphQlHandler());
builder.url(this.transport.getUrl());
builder.headers(headers -> headers.putAll(this.transport.getHeaders()));
Builder<?> builder = new Builder<>(this.transport);
this.builderInitializer.accept(builder);
return builder;
}
@@ -72,11 +72,20 @@ final class DefaultWebGraphQlTester extends AbstractDelegatingGraphQlTester impl
private final WebGraphQlHandler handler;
private CodecConfigurer codecConfigurer = ClientCodecConfigurer.create();
Builder(WebGraphQlHandler handler) {
Assert.notNull(handler, "WebGraphQlHandler is required");
this.handler = handler;
}
Builder(WebGraphQlHandlerTransport transport) {
this.url = transport.getUrl();
this.headers.putAll(transport.getHeaders());
this.handler = transport.getGraphQlHandler();
this.codecConfigurer = transport.getCodecConfigurer();
}
@Override
public B url(String url) {
return url(new DefaultUriBuilderFactory().uriString(url).build());
@@ -101,8 +110,8 @@ final class DefaultWebGraphQlTester extends AbstractDelegatingGraphQlTester impl
}
@Override
public B codecConfigurer(Consumer<CodecConfigurer> codecConsumer) {
// Ignore, no serialization needs at this level
public B codecConfigurer(Consumer<CodecConfigurer> codecConfigurerConsumer) {
codecConfigurerConsumer.accept(this.codecConfigurer);
return self();
}
@@ -113,11 +122,23 @@ final class DefaultWebGraphQlTester extends AbstractDelegatingGraphQlTester impl
@Override
public WebGraphQlTester build() {
WebGraphQlHandlerTransport transport = new WebGraphQlHandlerTransport(this.url, this.headers, this.handler);
registerJsonPathMappingProvider();
WebGraphQlHandlerTransport transport =
new WebGraphQlHandlerTransport(this.url, this.headers, this.handler, this.codecConfigurer);
GraphQlTester tester = super.buildGraphQlTester(transport);
return new DefaultWebGraphQlTester(tester, transport, getBuilderInitializer());
}
private void registerJsonPathMappingProvider() {
configureJsonPathConfig(jsonPathConfig -> {
CodecMappingProvider provider = new CodecMappingProvider(this.codecConfigurer);
return jsonPathConfig.mappingProvider(provider);
});
}
}
}

View File

@@ -25,6 +25,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.client.CodecMappingProvider;
import org.springframework.graphql.client.GraphQlClient;
import org.springframework.graphql.client.GraphQlTransport;
import org.springframework.graphql.client.WebSocketGraphQlClient;
@@ -44,12 +45,12 @@ final class DefaultWebSocketGraphQlTester extends AbstractDelegatingGraphQlTeste
private final WebSocketGraphQlClient webSocketGraphQlClient;
private final Consumer<GraphQlTester.Builder<?>> builderInitializer;
private final Consumer<AbstractGraphQlTesterBuilder<?>> builderInitializer;
DefaultWebSocketGraphQlTester(
GraphQlTester graphQlTester, WebSocketGraphQlClient webSocketGraphQlClient,
Consumer<GraphQlTester.Builder<?>> builderInitializer) {
Consumer<AbstractGraphQlTesterBuilder<?>> builderInitializer) {
super(graphQlTester);
this.webSocketGraphQlClient = webSocketGraphQlClient;
@@ -132,11 +133,21 @@ final class DefaultWebSocketGraphQlTester extends AbstractDelegatingGraphQlTeste
@Override
public WebSocketGraphQlTester build() {
registerJsonPathMappingProvider();
WebSocketGraphQlClient client = this.graphQlClientBuilder.build();
GraphQlTester graphQlTester = super.buildGraphQlTester(asTransport(client));
return new DefaultWebSocketGraphQlTester(graphQlTester, client, getBuilderInitializer());
}
private void registerJsonPathMappingProvider() {
this.graphQlClientBuilder.codecConfigurer(codecConfigurer -> {
configureJsonPathConfig(jsonPathConfig -> {
CodecMappingProvider provider = new CodecMappingProvider(codecConfigurer);
return jsonPathConfig.mappingProvider(provider);
});
});
}
/**
* GraphQlTransport implementations are private, but we can create the
* GraphQlClient for it and adapt it.

View File

@@ -26,6 +26,7 @@ import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInput;
import org.springframework.graphql.web.WebOutput;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.lang.Nullable;
@@ -43,11 +44,16 @@ final class WebGraphQlHandlerTransport extends AbstractDirectTransport {
private final WebGraphQlHandler graphQlHandler;
private final CodecConfigurer codecConfigurer;
WebGraphQlHandlerTransport(
@Nullable URI url, HttpHeaders headers, WebGraphQlHandler handler, CodecConfigurer configurer) {
WebGraphQlHandlerTransport(@Nullable URI url, HttpHeaders headers, WebGraphQlHandler graphQlHandler) {
this.url = (url != null ? url : URI.create(""));
this.headers.addAll(headers);
this.graphQlHandler = graphQlHandler;
this.graphQlHandler = handler;
this.codecConfigurer = configurer;
}
@@ -63,6 +69,10 @@ final class WebGraphQlHandlerTransport extends AbstractDirectTransport {
return this.graphQlHandler;
}
public CodecConfigurer getCodecConfigurer() {
return this.codecConfigurer;
}
@Override
protected Mono<WebOutput> executeInternal(GraphQlRequest request) {

View File

@@ -18,27 +18,37 @@ package org.springframework.graphql.test.tester;
import java.net.URI;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.graphql.RequestOutput;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.graphql.web.TestWebSocketClient;
import org.springframework.graphql.web.TestWebSocketConnection;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInput;
import org.springframework.graphql.web.WebInterceptor;
import org.springframework.graphql.web.WebOutput;
import org.springframework.graphql.web.webflux.GraphQlHttpHandler;
import org.springframework.graphql.web.webflux.GraphQlWebSocketHandler;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.lang.Nullable;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.socket.WebSocketHandler;
@@ -49,8 +59,8 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r
/**
* Tests for the builders of Web {@code GraphQlTester} extensions, using a
* {@link WebInterceptor} to capture the WebInput on the server side, and return
* with no handling.
* {@link WebInterceptor} to capture the WebInput on the server side, and
* optionally returning a mock response, or an empty response.
*
* <ul>
* <li>{@link HttpGraphQlTester} via {@link WebTestClient} to {@link GraphQlHttpHandler}
@@ -156,11 +166,40 @@ public class WebGraphQlTesterBuilderTests {
assertThat(input.getDocument()).isEqualTo(DOCUMENT);
}
@ParameterizedTest
@MethodSource("argumentSource")
void codecConfigurerRegistersJsonPathMappingProvider(TesterBuilderSetup builderSetup) {
TestJackson2JsonDecoder testDecoder = new TestJackson2JsonDecoder();
WebGraphQlTester.Builder<?> builder = builderSetup.initBuilder()
.codecConfigurer(codecConfigurer -> codecConfigurer.customCodecs().register(testDecoder));
String document = "{me {name}}";
MovieCharacter character = MovieCharacter.create("Luke Skywalker");
builderSetup.setMockResponse(document,
ExecutionResultImpl.newExecutionResult()
.data(Collections.singletonMap("me", character))
.build());
WebGraphQlTester client = builder.build();
GraphQlTester.Response response = client.document(document).execute();
testDecoder.resetLastValue();
assertThat(testDecoder.getLastValue()).isNull();
assertThat(response).isNotNull();
response.path("me").entity(MovieCharacter.class).isEqualTo(character);
assertThat(testDecoder.getLastValue()).isEqualTo(character);
}
private interface TesterBuilderSetup {
WebGraphQlTester.Builder<?> initBuilder();
void setMockResponse(String document, ExecutionResult result);
WebInput getWebInput();
}
@@ -170,22 +209,42 @@ public class WebGraphQlTesterBuilderTests {
private WebInput webInput;
private final Map<String, RequestOutput> responses = new HashMap<>();
public WebBuilderSetup() {
RequestOutput defaultResponse = new RequestOutput(
ExecutionInput.newExecutionInput().query(DOCUMENT).build(),
ExecutionResultImpl.newExecutionResult().build());
this.responses.put(DOCUMENT, defaultResponse);
}
@Override
public WebGraphQlTester.Builder<?> initBuilder() {
return WebGraphQlTester.builder(webGraphQlHandler());
}
protected WebGraphQlHandler webGraphQlHandler() {
return WebGraphQlHandler.builder(requestInput -> Mono.error(new UnsupportedOperationException()))
return WebGraphQlHandler.builder(requestInput -> {
String document = requestInput.getDocument();
RequestOutput output = this.responses.get(document);
Assert.notNull(output, "Unexpected request: " + document);
return Mono.just(output);
})
.interceptor((input, chain) -> {
this.webInput = input;
return Mono.just(new WebOutput(new RequestOutput(
ExecutionInput.newExecutionInput().query("{ notUsed }").build(),
ExecutionResultImpl.newExecutionResult().build())));
return chain.next(webInput);
})
.build();
}
@Override
public void setMockResponse(String document, ExecutionResult result) {
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(document).build();
this.responses.put(document, new RequestOutput(executionInput, result));
}
@Override
public WebInput getWebInput() {
return this.webInput;
@@ -217,4 +276,29 @@ public class WebGraphQlTesterBuilderTests {
}
private static class TestJackson2JsonDecoder extends Jackson2JsonDecoder {
@Nullable
private Object lastValue;
@Nullable
Object getLastValue() {
return this.lastValue;
}
@Override
public Object decode(DataBuffer dataBuffer, ResolvableType targetType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException {
this.lastValue = super.decode(dataBuffer, targetType, mimeType, hints);
return this.lastValue;
}
void resetLastValue() {
this.lastValue = null;
}
}
}

View File

@@ -17,16 +17,15 @@
package org.springframework.graphql.client;
import java.util.function.Consumer;
import java.util.function.Function;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import org.springframework.graphql.support.CachingDocumentSource;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.graphql.support.ResourceDocumentSource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -45,17 +44,13 @@ import org.springframework.util.ClassUtils;
*/
public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClientBuilder<B>> implements GraphQlClient.Builder<B> {
private static final boolean jackson2Present;
static {
ClassLoader classLoader = AbstractGraphQlClientBuilder.class.getClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
}
private static final boolean jackson2Present = ClassUtils.isPresent(
"com.fasterxml.jackson.databind.ObjectMapper", AbstractGraphQlClientBuilder.class.getClassLoader());
@Nullable
private DocumentSource documentSource;
private DocumentSource documentSource = new CachingDocumentSource(new ResourceDocumentSource());
private Configuration jsonPathConfig = Configuration.builder().build();
/**
@@ -78,46 +73,52 @@ public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClie
return (T) this;
}
// Protected methods for use from build() in subclasses
/**
* Subclasses call this from {@link #build()} to provide the transport and get
* the default {@code GraphQlClient to delegate to for request execution.
* Allow transport-specific subclass builders to register a JSON Path
* {@link MappingProvider} that matches the JSON encoding/decoding they use.
*/
protected void configureJsonPathConfig(Function<Configuration, Configuration> configurer) {
this.jsonPathConfig = configurer.apply(this.jsonPathConfig);
}
/**
* Build the default transport-agnostic client that subclasses can then wrap
* with {@link AbstractDelegatingGraphQlClient}.
*/
protected GraphQlClient buildGraphQlClient(GraphQlTransport transport) {
Assert.notNull(transport, "GraphQlTransport is required");
return new DefaultGraphQlClient(transport, initJsonPathConfig(), initDocumentSource(), getBuilderInitializer());
}
private Configuration initJsonPathConfig() {
// Allow configuring JSONPath with codecs from transport subclasses
return (jackson2Present ? Jackson2Configuration.create() : Configuration.builder().build());
}
if (jackson2Present) {
configureJsonPathConfig(Jackson2Configurer::configure);
}
private DocumentSource initDocumentSource() {
return (this.documentSource == null ?
new CachingDocumentSource(new ResourceDocumentSource()) : this.documentSource);
return new DefaultGraphQlClient(
transport, this.jsonPathConfig, this.documentSource, getBuilderInitializer());
}
/**
* Subclasses call this from {@link #build()} to obtain a {@code Consumer} to
* initialize new builder instances with, based on "this" builder.
* Return a {@code Consumer} to initialize new builders from "this" builder.
*/
protected Consumer<GraphQlClient.Builder<?>> getBuilderInitializer() {
protected Consumer<AbstractGraphQlClientBuilder<?>> getBuilderInitializer() {
return builder -> {
if (this.documentSource != null) {
builder.documentSource(documentSource);
}
builder.documentSource(documentSource);
builder.configureJsonPathConfig(config -> this.jsonPathConfig);
};
}
private static class Jackson2Configuration {
private static class Jackson2Configurer {
static Configuration create() {
return Configuration.builder()
.jsonProvider(new JacksonJsonProvider())
.mappingProvider(new JacksonMappingProvider())
.build();
private static final MappingProvider defaultProvider = Configuration.defaultConfiguration().mappingProvider();
static Configuration configure(Configuration config) {
return (config.mappingProvider() != null && config.mappingProvider() != defaultProvider ? config :
config.mappingProvider(new JacksonMappingProvider()));
}
}
}

View File

@@ -38,30 +38,30 @@ import org.springframework.web.reactive.socket.WebSocketSession;
* @author Rossen Stoyanchev
* @since 1.0.0
*/
final class WebSocketCodecDelegate {
final class CodecDelegate {
private static final ResolvableType MESSAGE_TYPE = ResolvableType.forClass(GraphQlWebSocketMessage.class);
private final CodecConfigurer configurer;
private final CodecConfigurer codecConfigurer;
private final Decoder<?> decoder;
private final Encoder<?> encoder;
WebSocketCodecDelegate() {
CodecDelegate() {
this(ClientCodecConfigurer.create());
}
WebSocketCodecDelegate(CodecConfigurer configurer) {
CodecDelegate(CodecConfigurer configurer) {
Assert.notNull(configurer, "CodecConfigurer is required");
this.configurer = configurer;
this.decoder = initDecoder(configurer);
this.encoder = initEncoder(configurer);
this.codecConfigurer = configurer;
this.decoder = findJsonDecoder(configurer);
this.encoder = findJsonEncoder(configurer);
}
private static Decoder<?> initDecoder(CodecConfigurer configurer) {
static Decoder<?> findJsonDecoder(CodecConfigurer configurer) {
return configurer.getReaders().stream()
.filter((reader) -> reader.canRead(MESSAGE_TYPE, MediaType.APPLICATION_JSON))
.map((reader) -> ((DecoderHttpMessageReader<?>) reader).getDecoder())
@@ -69,7 +69,7 @@ final class WebSocketCodecDelegate {
.orElseThrow(() -> new IllegalArgumentException("No JSON Decoder"));
}
private static Encoder<?> initEncoder(CodecConfigurer configurer) {
static Encoder<?> findJsonEncoder(CodecConfigurer configurer) {
return configurer.getWriters().stream()
.filter((writer) -> writer.canWrite(MESSAGE_TYPE, MediaType.APPLICATION_JSON))
.map((writer) -> ((EncoderHttpMessageWriter<?>) writer).getEncoder())
@@ -79,7 +79,7 @@ final class WebSocketCodecDelegate {
public CodecConfigurer getCodecConfigurer() {
return this.configurer;
return this.codecConfigurer;
}
@@ -98,5 +98,4 @@ final class WebSocketCodecDelegate {
return (GraphQlWebSocketMessage) this.decoder.decode(buffer, MESSAGE_TYPE, null, null);
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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.Collections;
import java.util.Map;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.TypeRef;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.lang.Nullable;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* JSON Path {@link MappingProvider} that uses {@link Encoder} and {@link Decoder}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public final class CodecMappingProvider implements MappingProvider {
private final Encoder<?> encoder;
private final Decoder<?> decoder;
/**
* Create an instance by finding the first JSON {@link Encoder} and
* {@link Decoder} in the given {@link CodecConfigurer}.
* @throws IllegalArgumentException if there is no JSON encoder or decoder.
*/
public CodecMappingProvider(CodecConfigurer configurer) {
this.encoder = CodecDelegate.findJsonEncoder(configurer);
this.decoder = CodecDelegate.findJsonDecoder(configurer);
}
@Nullable
@Override
public <T> T map(Object source, Class<T> targetType, Configuration configuration) {
return mapToTargetType(source, ResolvableType.forClass(targetType));
}
@Nullable
@Override
public <T> T map(Object source, TypeRef<T> targetType, Configuration configuration) {
return mapToTargetType(source, ResolvableType.forType(targetType.getType()));
}
@SuppressWarnings("unchecked")
@Nullable
private <T> T mapToTargetType(Object source, ResolvableType targetType) {
DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
MimeType mimeType = MimeTypeUtils.APPLICATION_JSON;
Map<String, Object> hints = Collections.emptyMap();
DataBuffer buffer = ((Encoder<T>) this.encoder).encodeValue(
(T) source, bufferFactory, ResolvableType.forInstance(source), mimeType, hints);
return ((Decoder<T>) this.decoder).decode(buffer, targetType, mimeType, hints);
}
}

View File

@@ -52,17 +52,17 @@ final class DefaultGraphQlClient implements GraphQlClient {
private final DocumentSource documentSource;
private final Consumer<GraphQlClient.Builder<?>> builderInitializer;
private final Consumer<AbstractGraphQlClientBuilder<?>> builderInitializer;
DefaultGraphQlClient(
GraphQlTransport transport, Configuration jsonPathConfig, DocumentSource documentSource,
Consumer<GraphQlClient.Builder<?>> builderInitializer) {
Consumer<AbstractGraphQlClientBuilder<?>> builderInitializer) {
Assert.notNull(transport, "GraphQlTransport is required");
Assert.notNull(jsonPathConfig, "JSONPath Configuration is required");
Assert.notNull(documentSource, "DocumentSource is required");
Assert.notNull(documentSource, "`builderInitializer` is required");
Assert.notNull(builderInitializer, "`builderInitializer` is required");
this.transport = transport;
this.jsonPathConfig = jsonPathConfig;

View File

@@ -38,11 +38,11 @@ final class DefaultHttpGraphQlClient extends AbstractDelegatingGraphQlClient imp
private final WebClient webClient;
private final Consumer<GraphQlClient.Builder<?>> builderInitializer;
private final Consumer<AbstractGraphQlClientBuilder<?>> builderInitializer;
DefaultHttpGraphQlClient(GraphQlClient graphQlClient, WebClient webClient,
Consumer<GraphQlClient.Builder<?>> builderInitializer) {
Consumer<AbstractGraphQlClientBuilder<?>> builderInitializer) {
super(graphQlClient);
@@ -90,7 +90,6 @@ final class DefaultHttpGraphQlClient extends AbstractDelegatingGraphQlClient imp
this.webClientBuilder = clientBuilder;
}
@Override
public Builder url(String url) {
this.webClientBuilder.baseUrl(url);
@@ -117,8 +116,8 @@ final class DefaultHttpGraphQlClient extends AbstractDelegatingGraphQlClient imp
}
@Override
public Builder codecConfigurer(Consumer<CodecConfigurer> codecsConsumer) {
this.webClientBuilder.codecs(codecsConsumer::accept);
public Builder codecConfigurer(Consumer<CodecConfigurer> codecConfigurerConsumer) {
this.webClientBuilder.codecs(codecConfigurerConsumer::accept);
return this;
}
@@ -130,11 +129,22 @@ final class DefaultHttpGraphQlClient extends AbstractDelegatingGraphQlClient imp
@Override
public HttpGraphQlClient build() {
registerJsonPathMappingProvider();
WebClient webClient = this.webClientBuilder.build();
GraphQlClient graphQlClient = super.buildGraphQlClient(new HttpGraphQlTransport(webClient));
return new DefaultHttpGraphQlClient(graphQlClient, webClient, getBuilderInitializer());
}
private void registerJsonPathMappingProvider() {
this.webClientBuilder.codecs(codecConfigurer ->
configureJsonPathConfig(config -> {
CodecMappingProvider provider = new CodecMappingProvider(codecConfigurer);
return config.mappingProvider(provider);
}));
}
}
}

View File

@@ -41,11 +41,11 @@ final class DefaultWebSocketGraphQlClient extends AbstractDelegatingGraphQlClien
private final WebSocketGraphQlTransport transport;
private final Consumer<GraphQlClient.Builder<?>> builderInitializer;
private final Consumer<AbstractGraphQlClientBuilder<?>> builderInitializer;
DefaultWebSocketGraphQlClient(GraphQlClient delegate, WebSocketGraphQlTransport transport,
Consumer<GraphQlClient.Builder<?>> builderInitializer) {
Consumer<AbstractGraphQlClientBuilder<?>> builderInitializer) {
super(delegate);
@@ -134,14 +134,16 @@ final class DefaultWebSocketGraphQlClient extends AbstractDelegatingGraphQlClien
}
@Override
public Builder codecConfigurer(Consumer<CodecConfigurer> codecConsumer) {
codecConsumer.accept(this.codecConfigurer);
public Builder codecConfigurer(Consumer<CodecConfigurer> codecConfigurerConsumer) {
codecConfigurerConsumer.accept(this.codecConfigurer);
return this;
}
@Override
public WebSocketGraphQlClient build() {
registerJsonPathMappingProvider();
WebSocketGraphQlTransport transport = new WebSocketGraphQlTransport(
this.url, this.headers, this.webSocketClient, this.codecConfigurer, null, payload -> {});
@@ -149,6 +151,13 @@ final class DefaultWebSocketGraphQlClient extends AbstractDelegatingGraphQlClien
return new DefaultWebSocketGraphQlClient(graphQlClient, transport, getBuilderInitializer());
}
private void registerJsonPathMappingProvider() {
configureJsonPathConfig(jsonPathConfig -> {
CodecMappingProvider provider = new CodecMappingProvider(this.codecConfigurer);
return jsonPathConfig.mappingProvider(provider);
});
}
}
}

View File

@@ -166,7 +166,7 @@ final class WebSocketGraphQlTransport implements GraphQlTransport {
*/
private static class GraphQlSessionHandler implements WebSocketHandler {
private final WebSocketCodecDelegate codecDelegate;
private final CodecDelegate codecDelegate;
private final GraphQlWebSocketMessage connectionInitMessage;
@@ -180,7 +180,7 @@ final class WebSocketGraphQlTransport implements GraphQlTransport {
GraphQlSessionHandler(CodecConfigurer codecConfigurer,
@Nullable Object connectionInitPayload, Consumer<Map<String, Object>> connectionAckHandler) {
this.codecDelegate = new WebSocketCodecDelegate(codecConfigurer);
this.codecDelegate = new CodecDelegate(codecConfigurer);
this.connectionInitMessage = GraphQlWebSocketMessage.connectionInit(connectionInitPayload);
this.connectionAckHandler = connectionAckHandler;
this.graphQlSessionSink = Sinks.unsafe().one();

View File

@@ -34,12 +34,13 @@ import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.WebSocketSession;
/**
* WebFlux Support class for GraphQL over WebSocket handling.
* Delegate that can be embedded in a class to help with encoding and decoding
* GraphQL over WebSocket messages.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
final class WebSocketCodecDelegate {
final class CodecDelegate {
private static final ResolvableType MESSAGE_TYPE = ResolvableType.forClass(GraphQlWebSocketMessage.class);
@@ -49,13 +50,13 @@ final class WebSocketCodecDelegate {
private final Encoder<?> encoder;
WebSocketCodecDelegate(CodecConfigurer codecConfigurer) {
CodecDelegate(CodecConfigurer codecConfigurer) {
Assert.notNull(codecConfigurer, "CodecConfigurer is required");
this.decoder = initDecoder(codecConfigurer);
this.encoder = initEncoder(codecConfigurer);
this.decoder = findJsonDecoder(codecConfigurer);
this.encoder = findJsonEncoder(codecConfigurer);
}
private static Decoder<?> initDecoder(CodecConfigurer configurer) {
private static Decoder<?> findJsonDecoder(CodecConfigurer configurer) {
return configurer.getReaders().stream()
.filter((reader) -> reader.canRead(MESSAGE_TYPE, MediaType.APPLICATION_JSON))
.map((reader) -> ((DecoderHttpMessageReader<?>) reader).getDecoder())
@@ -63,7 +64,7 @@ final class WebSocketCodecDelegate {
.orElseThrow(() -> new IllegalArgumentException("No JSON Decoder"));
}
private static Encoder<?> initEncoder(CodecConfigurer configurer) {
private static Encoder<?> findJsonEncoder(CodecConfigurer configurer) {
return configurer.getWriters().stream()
.filter((writer) -> writer.canWrite(MESSAGE_TYPE, MediaType.APPLICATION_JSON))
.map((writer) -> ((EncoderHttpMessageWriter<?>) writer).getEncoder())
@@ -72,31 +73,25 @@ final class WebSocketCodecDelegate {
}
@SuppressWarnings("ConstantConditions")
public GraphQlWebSocketMessage decode(WebSocketMessage webSocketMessage) {
DataBuffer buffer = DataBufferUtils.retain(webSocketMessage.getPayload());
return (GraphQlWebSocketMessage) this.decoder.decode(buffer, MESSAGE_TYPE, null, null);
}
public WebSocketMessage encodeConnectionAckMessage(WebSocketSession session, Object ackPayload) {
public WebSocketMessage encodeConnectionAck(WebSocketSession session, Object ackPayload) {
return encode(session, GraphQlWebSocketMessage.connectionAck(ackPayload));
}
public WebSocketMessage encodeNextMessage(WebSocketSession session, String id, ExecutionResult result) {
public WebSocketMessage encodeNext(WebSocketSession session, String id, ExecutionResult result) {
return encode(session, GraphQlWebSocketMessage.next(id, result));
}
public WebSocketMessage encodeErrorMessage(WebSocketSession session, String id, Throwable ex) {
public WebSocketMessage encodeError(WebSocketSession session, String id, Throwable ex) {
GraphQLError error = GraphqlErrorBuilder.newError().message(ex.getMessage()).build();
return encode(session, GraphQlWebSocketMessage.error(id, error));
}
public WebSocketMessage encodeCompleteMessage(WebSocketSession session, String id) {
public WebSocketMessage encodeComplete(WebSocketSession session, String id) {
return encode(session, GraphQlWebSocketMessage.complete(id));
}
@SuppressWarnings("unchecked")
private <T> WebSocketMessage encode(WebSocketSession session, GraphQlWebSocketMessage message) {
public <T> WebSocketMessage encode(WebSocketSession session, GraphQlWebSocketMessage message) {
DataBuffer buffer = ((Encoder<T>) this.encoder).encodeValue(
(T) message, session.bufferFactory(), MESSAGE_TYPE, MimeTypeUtils.APPLICATION_JSON, null);
@@ -104,4 +99,10 @@ final class WebSocketCodecDelegate {
return new WebSocketMessage(WebSocketMessage.Type.TEXT, buffer);
}
@SuppressWarnings("ConstantConditions")
public GraphQlWebSocketMessage decode(WebSocketMessage webSocketMessage) {
DataBuffer buffer = DataBufferUtils.retain(webSocketMessage.getPayload());
return (GraphQlWebSocketMessage) this.decoder.decode(buffer, MESSAGE_TYPE, null, null);
}
}

View File

@@ -61,7 +61,7 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
private final WebGraphQlHandler graphQlHandler;
private final WebSocketCodecDelegate codecDelegate;
private final CodecDelegate codecDelegate;
private final Duration initTimeoutDuration;
@@ -78,7 +78,7 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
Assert.notNull(graphQlHandler, "WebGraphQlHandler is required");
this.graphQlHandler = graphQlHandler;
this.codecDelegate = new WebSocketCodecDelegate(codecConfigurer);
this.codecDelegate = new CodecDelegate(codecConfigurer);
this.initTimeoutDuration = connectionInitTimeout;
}
@@ -144,7 +144,7 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
}
return this.graphQlHandler.handleWebSocketInitialization(payload)
.defaultIfEmpty(Collections.emptyMap())
.map(ackPayload -> this.codecDelegate.encodeConnectionAckMessage(session, ackPayload))
.map(ackPayload -> this.codecDelegate.encodeConnectionAck(session, ackPayload))
.flux()
.onErrorResume(ex -> GraphQlStatus.close(session, GraphQlStatus.UNAUTHORIZED_STATUS));
default:
@@ -182,14 +182,14 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
}
return outputFlux
.map(result -> this.codecDelegate.encodeNextMessage(session, id, result))
.concatWith(Mono.fromCallable(() -> this.codecDelegate.encodeCompleteMessage(session, id)))
.map(result -> this.codecDelegate.encodeNext(session, id, result))
.concatWith(Mono.fromCallable(() -> this.codecDelegate.encodeComplete(session, id)))
.onErrorResume(ex -> {
if (ex instanceof SubscriptionExistsException) {
CloseStatus status = new CloseStatus(4409, "Subscriber for " + id + " already exists");
return GraphQlStatus.close(session, status);
}
return Mono.fromCallable(() -> this.codecDelegate.encodeErrorMessage(session, id, ex));
return Mono.fromCallable(() -> this.codecDelegate.encodeError(session, id, ex));
});
}

View File

@@ -51,7 +51,7 @@ public final class MockGraphQlWebSocketServer implements WebSocketHandler {
private final Map<Map<String, Object>, Exchange> expectedExchanges = new LinkedHashMap<>();
private final WebSocketCodecDelegate codecDelegate = new WebSocketCodecDelegate();
private final CodecDelegate codecDelegate = new CodecDelegate();
/**

View File

@@ -60,7 +60,7 @@ public class MockWebSocketGraphQlTransportTests {
private final static Duration TIMEOUT = Duration.ofSeconds(5);
private static final WebSocketCodecDelegate CODEC_DELEGATE = new WebSocketCodecDelegate();
private static final CodecDelegate CODEC_DELEGATE = new CodecDelegate();
private final MockGraphQlWebSocketServer mockServer = new MockGraphQlWebSocketServer();
@@ -313,7 +313,7 @@ public class MockWebSocketGraphQlTransportTests {
*/
private static class UnexpectedResponseHandler implements WebSocketHandler {
private final WebSocketCodecDelegate codecDelegate = new WebSocketCodecDelegate();
private final CodecDelegate codecDelegate = new CodecDelegate();
@Override

View File

@@ -18,28 +18,38 @@ package org.springframework.graphql.client;
import java.net.URI;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.graphql.RequestOutput;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.graphql.web.TestWebSocketClient;
import org.springframework.graphql.web.TestWebSocketConnection;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInput;
import org.springframework.graphql.web.WebInterceptor;
import org.springframework.graphql.web.WebOutput;
import org.springframework.graphql.web.webflux.GraphQlHttpHandler;
import org.springframework.graphql.web.webflux.GraphQlWebSocketHandler;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.lang.Nullable;
import org.springframework.test.web.reactive.server.HttpHandlerConnector;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.RouterFunction;
@@ -52,8 +62,8 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r
/**
* Tests for the builders of Web {@code GraphQlClient} extensions, using a
* {@link WebInterceptor} to capture the WebInput on the server side, and return
* with no handling.
* {@link WebInterceptor} to capture the WebInput on the server side, and
* optionally returning a mock response, or an empty response.
*
* <ul>
* <li>{@link HttpGraphQlClient} via {@link HttpHandlerConnector} to {@link GraphQlHttpHandler}
@@ -162,7 +172,7 @@ public class WebGraphQlClientBuilderTests {
@ParameterizedTest
@MethodSource("argumentSource")
void url(ClientBuilderSetup builderSetup) {
void urlEncoding(ClientBuilderSetup builderSetup) {
WebGraphQlClient client = builderSetup.initBuilder().url("/graphql one").build();
client.document(DOCUMENT).execute().block(TIMEOUT);
@@ -170,12 +180,40 @@ public class WebGraphQlClientBuilderTests {
assertThat(builderSetup.getWebInput().getUri().toString()).isEqualTo("/graphql%20one");
}
@ParameterizedTest
@MethodSource("argumentSource")
void codecConfigurerRegistersJsonPathMappingProvider(ClientBuilderSetup builderSetup) {
TestJackson2JsonDecoder testDecoder = new TestJackson2JsonDecoder();
WebGraphQlClient.Builder<?> builder = builderSetup.initBuilder()
.codecConfigurer(codecConfigurer -> codecConfigurer.customCodecs().register(testDecoder));
String document = "{me {name}}";
MovieCharacter character = MovieCharacter.create("Luke Skywalker");
builderSetup.setMockResponse(document,
ExecutionResultImpl.newExecutionResult()
.data(Collections.singletonMap("me", character))
.build());
WebGraphQlClient client = builder.build();
GraphQlClient.Response response = client.document(document).execute().block(TIMEOUT);
testDecoder.resetLastValue();
assertThat(testDecoder.getLastValue()).isNull();
assertThat(response).isNotNull();
assertThat(response.toEntity("me", MovieCharacter.class).getName()).isEqualTo("Luke Skywalker");
assertThat(testDecoder.getLastValue()).isEqualTo(character);
}
private interface ClientBuilderSetup {
WebGraphQlClient.Builder<?> initBuilder();
void setMockResponse(String document, ExecutionResult result);
WebInput getWebInput();
}
@@ -185,17 +223,37 @@ public class WebGraphQlClientBuilderTests {
private WebInput webInput;
private final Map<String, RequestOutput> responses = new HashMap<>();
public AbstractBuilderSetup() {
RequestOutput defaultResponse = new RequestOutput(
ExecutionInput.newExecutionInput().query(DOCUMENT).build(),
ExecutionResultImpl.newExecutionResult().build());
this.responses.put(DOCUMENT, defaultResponse);
}
protected WebGraphQlHandler webGraphQlHandler() {
return WebGraphQlHandler.builder(requestInput -> Mono.error(new UnsupportedOperationException()))
return WebGraphQlHandler.builder(requestInput -> {
String document = requestInput.getDocument();
RequestOutput output = this.responses.get(document);
Assert.notNull(output, "Unexpected request: " + document);
return Mono.just(output);
})
.interceptor((input, chain) -> {
this.webInput = input;
return Mono.just(new WebOutput(new RequestOutput(
ExecutionInput.newExecutionInput().query("{ notUsed }").build(),
ExecutionResultImpl.newExecutionResult().build())));
return chain.next(webInput);
})
.build();
}
@Override
public void setMockResponse(String document, ExecutionResult result) {
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(document).build();
this.responses.put(document, new RequestOutput(executionInput, result));
}
@Override
public WebInput getWebInput() {
return this.webInput;
@@ -229,4 +287,30 @@ public class WebGraphQlClientBuilderTests {
}
private static class TestJackson2JsonDecoder extends Jackson2JsonDecoder {
@Nullable
private Object lastValue;
@Nullable
Object getLastValue() {
return this.lastValue;
}
@Override
public Object decode(DataBuffer dataBuffer, ResolvableType targetType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException {
this.lastValue = super.decode(dataBuffer, targetType, mimeType, hints);
return this.lastValue;
}
void resetLastValue() {
this.lastValue = null;
}
}
}