diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java index 2bdb0e53..9f68512c 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java @@ -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> implements GraphQlTester.Builder { - 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 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> getBuilderInitializer() { + protected Consumer> 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())); } + } } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java index 151afcce..7b670221 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java @@ -34,11 +34,11 @@ final class DefaultGraphQlServiceTester extends AbstractDelegatingGraphQlTester private final GraphQlServiceTransport transport; - private final Consumer> builderInitializer; + private final Consumer> builderInitializer; DefaultGraphQlServiceTester(GraphQlTester tester, GraphQlServiceTransport transport, - Consumer> builderInitializer) { + Consumer> builderInitializer) { super(tester); diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java index 2f631297..41e59655 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java @@ -69,7 +69,7 @@ final class DefaultGraphQlTester implements GraphQlTester { private final Duration responseTimeout; - private final Consumer> builderInitializer; + private final Consumer> builderInitializer; /** @@ -78,7 +78,12 @@ final class DefaultGraphQlTester implements GraphQlTester { DefaultGraphQlTester( GraphQlTransport transport, @Nullable Predicate errorFilter, Configuration jsonPathConfig, DocumentSource documentSource, Duration timeout, - Consumer> builderInitializer) { + Consumer> 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; diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java index 46e6d708..ece8dc28 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java @@ -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> builderInitializer; + private final Consumer> builderInitializer; DefaultHttpGraphQlTester(GraphQlTester graphQlTester, WebTestClient webTestClient, - Consumer> builderInitializer) { + Consumer> 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); + })); + } + } } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java index 86a52656..f0e0bdd9 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java @@ -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> builderInitializer; + private final Consumer> builderInitializer; DefaultWebGraphQlTester(GraphQlTester tester, WebGraphQlHandlerTransport transport, - Consumer> builderInitializer) { + Consumer> 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 codecConsumer) { - // Ignore, no serialization needs at this level + public B codecConfigurer(Consumer 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); + }); + } + } } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java index 42b2339f..d5dbd08d 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java @@ -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> builderInitializer; + private final Consumer> builderInitializer; DefaultWebSocketGraphQlTester( GraphQlTester graphQlTester, WebSocketGraphQlClient webSocketGraphQlClient, - Consumer> builderInitializer) { + Consumer> 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. diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlHandlerTransport.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlHandlerTransport.java index ed3bad49..ed6a37f5 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlHandlerTransport.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlHandlerTransport.java @@ -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 executeInternal(GraphQlRequest request) { diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterBuilderTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterBuilderTests.java index f3d349c0..fb3dcac4 100644 --- a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterBuilderTests.java +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterBuilderTests.java @@ -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. * *
    *
  • {@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 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 hints) throws DecodingException { + + this.lastValue = super.decode(dataBuffer, targetType, mimeType, hints); + return this.lastValue; + } + + void resetLastValue() { + this.lastValue = null; + } + + } + } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java index 94e3a97e..da0e2484 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java @@ -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> implements GraphQlClient.Builder { - 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 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> getBuilderInitializer() { + protected Consumer> 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())); } + } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketCodecDelegate.java b/spring-graphql/src/main/java/org/springframework/graphql/client/CodecDelegate.java similarity index 88% rename from spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketCodecDelegate.java rename to spring-graphql/src/main/java/org/springframework/graphql/client/CodecDelegate.java index 8e95640d..0e183176 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketCodecDelegate.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/CodecDelegate.java @@ -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); } - } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/CodecMappingProvider.java b/spring-graphql/src/main/java/org/springframework/graphql/client/CodecMappingProvider.java new file mode 100644 index 00000000..75b11111 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/CodecMappingProvider.java @@ -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 map(Object source, Class targetType, Configuration configuration) { + return mapToTargetType(source, ResolvableType.forClass(targetType)); + } + + @Nullable + @Override + public T map(Object source, TypeRef targetType, Configuration configuration) { + return mapToTargetType(source, ResolvableType.forType(targetType.getType())); + } + + @SuppressWarnings("unchecked") + @Nullable + private T mapToTargetType(Object source, ResolvableType targetType) { + + DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; + MimeType mimeType = MimeTypeUtils.APPLICATION_JSON; + Map hints = Collections.emptyMap(); + + DataBuffer buffer = ((Encoder) this.encoder).encodeValue( + (T) source, bufferFactory, ResolvableType.forInstance(source), mimeType, hints); + + return ((Decoder) this.decoder).decode(buffer, targetType, mimeType, hints); + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java index ae4811a6..ccd78e94 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java @@ -52,17 +52,17 @@ final class DefaultGraphQlClient implements GraphQlClient { private final DocumentSource documentSource; - private final Consumer> builderInitializer; + private final Consumer> builderInitializer; DefaultGraphQlClient( GraphQlTransport transport, Configuration jsonPathConfig, DocumentSource documentSource, - Consumer> builderInitializer) { + Consumer> 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; diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultHttpGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultHttpGraphQlClient.java index 1bf52dcb..bccf4bf7 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultHttpGraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultHttpGraphQlClient.java @@ -38,11 +38,11 @@ final class DefaultHttpGraphQlClient extends AbstractDelegatingGraphQlClient imp private final WebClient webClient; - private final Consumer> builderInitializer; + private final Consumer> builderInitializer; DefaultHttpGraphQlClient(GraphQlClient graphQlClient, WebClient webClient, - Consumer> builderInitializer) { + Consumer> 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 codecsConsumer) { - this.webClientBuilder.codecs(codecsConsumer::accept); + public Builder codecConfigurer(Consumer 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); + })); + } + } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultWebSocketGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultWebSocketGraphQlClient.java index 96819733..9bc1c44a 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultWebSocketGraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultWebSocketGraphQlClient.java @@ -41,11 +41,11 @@ final class DefaultWebSocketGraphQlClient extends AbstractDelegatingGraphQlClien private final WebSocketGraphQlTransport transport; - private final Consumer> builderInitializer; + private final Consumer> builderInitializer; DefaultWebSocketGraphQlClient(GraphQlClient delegate, WebSocketGraphQlTransport transport, - Consumer> builderInitializer) { + Consumer> builderInitializer) { super(delegate); @@ -134,14 +134,16 @@ final class DefaultWebSocketGraphQlClient extends AbstractDelegatingGraphQlClien } @Override - public Builder codecConfigurer(Consumer codecConsumer) { - codecConsumer.accept(this.codecConfigurer); + public Builder codecConfigurer(Consumer 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); + }); + } + } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java index 651c29d6..7400c0a4 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java @@ -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> connectionAckHandler) { - this.codecDelegate = new WebSocketCodecDelegate(codecConfigurer); + this.codecDelegate = new CodecDelegate(codecConfigurer); this.connectionInitMessage = GraphQlWebSocketMessage.connectionInit(connectionInitPayload); this.connectionAckHandler = connectionAckHandler; this.graphQlSessionSink = Sinks.unsafe().one(); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/WebSocketCodecDelegate.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/CodecDelegate.java similarity index 78% rename from spring-graphql/src/main/java/org/springframework/graphql/web/webflux/WebSocketCodecDelegate.java rename to spring-graphql/src/main/java/org/springframework/graphql/web/webflux/CodecDelegate.java index 2336461b..a9ca8714 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/WebSocketCodecDelegate.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/CodecDelegate.java @@ -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 WebSocketMessage encode(WebSocketSession session, GraphQlWebSocketMessage message) { + public WebSocketMessage encode(WebSocketSession session, GraphQlWebSocketMessage message) { DataBuffer buffer = ((Encoder) 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); + } + } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandler.java index dbc0934b..0d03a7d4 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/web/webflux/GraphQlWebSocketHandler.java @@ -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)); }); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/client/MockGraphQlWebSocketServer.java b/spring-graphql/src/test/java/org/springframework/graphql/client/MockGraphQlWebSocketServer.java index e59b8621..b4245f76 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/client/MockGraphQlWebSocketServer.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/client/MockGraphQlWebSocketServer.java @@ -51,7 +51,7 @@ public final class MockGraphQlWebSocketServer implements WebSocketHandler { private final Map, Exchange> expectedExchanges = new LinkedHashMap<>(); - private final WebSocketCodecDelegate codecDelegate = new WebSocketCodecDelegate(); + private final CodecDelegate codecDelegate = new CodecDelegate(); /** diff --git a/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java b/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java index 1995bcf7..20370603 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java @@ -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 diff --git a/spring-graphql/src/test/java/org/springframework/graphql/client/WebGraphQlClientBuilderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/client/WebGraphQlClientBuilderTests.java index ad89a785..e41d76d2 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/client/WebGraphQlClientBuilderTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/client/WebGraphQlClientBuilderTests.java @@ -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. * *
      *
    • {@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 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 hints) throws DecodingException { + + this.lastValue = super.decode(dataBuffer, targetType, mimeType, hints); + return this.lastValue; + } + + void resetLastValue() { + this.lastValue = null; + } + + } + + }