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:
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user