Simplify GraphQlClient hierarchy and implementations

Add WebGraphQlClient as a common representation for a Web GraphQlClient
extension and its builders that has to be started through the HTTP or
WebSocket implementations.

Remove the common base builder for HTTP and WebSocket, with HTTP simply
delegating to the underlying WebClient builder For WebSocket, builder
state is exposed from the transport to simplify the mutation logic.

DefaultGraphQlClientBuilder is now abstract, leaving subclasses to
implement the build method.

See gh-10
This commit is contained in:
rstoyanchev
2022-03-03 10:15:29 +00:00
parent 347f51941a
commit d7f5e44ad6
14 changed files with 375 additions and 333 deletions

View File

@@ -20,15 +20,16 @@ package org.springframework.graphql.client;
import org.springframework.util.Assert;
/**
* Base class for extensions of {@link GraphQlClient} that mainly assist with
* building the underlying transport, but otherwise delegate to the default
* {@link GraphQlClient} implementation for actual request execution.
* Base class for {@link GraphQlClient} extensions that assist with building an
* underlying transport, but otherwise delegate to the default
* {@link GraphQlClient} implementation to execute requests.
*
* <p>Subclasses must implement {@link GraphQlClient#mutate()} to allow mutation
* of both {@code GraphQlClient} and {@code GraphQlTransport} configuration.
* <p>Subclasses must implement {@link GraphQlClient#mutate()} to return a
* builder for the specific {@code GraphQlClient} extension.
*
* @author Rossen Stoyanchev
* @since 1.0.0
* @see AbstractGraphQlClientBuilder
*/
public abstract class AbstractDelegatingGraphQlClient implements GraphQlClient {

View File

@@ -31,54 +31,44 @@ import org.springframework.util.ClassUtils;
/**
* Default {@link GraphQlClient.Builder} implementation that builds a
* {@link GraphQlClient} for use with any transport.
* Abstract, base class for transport specific {@link GraphQlClient.Builder}
* implementations.
*
* <p>Intended for use as a base class for builders that do assist with building
* the underlying transport. Such extension
* <p>Subclasses must implement {@link #build()} and call
* {@link #buildGraphQlClient(GraphQlTransport)} to obtain a default, transport
* agnostic {@code GraphQlClient}. A transport specific extension can then wrap
* this default tester by extending {@link AbstractDelegatingGraphQlClient}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
* @see AbstractDelegatingGraphQlClient
*/
public class DefaultGraphQlClientBuilder<B extends DefaultGraphQlClientBuilder<B>> implements GraphQlClient.Builder<B> {
public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClientBuilder<B>> implements GraphQlClient.Builder<B> {
private static final boolean jackson2Present;
static {
ClassLoader classLoader = DefaultGraphQlClientBuilder.class.getClassLoader();
ClassLoader classLoader = AbstractGraphQlClientBuilder.class.getClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
}
@Nullable
private GraphQlTransport transport;
@Nullable
private DocumentSource documentSource;
/**
* Constructor with a given transport instance.
*/
DefaultGraphQlClientBuilder(GraphQlTransport transport) {
Assert.notNull(transport, "GraphQlTransport is required");
this.transport = transport;
}
/**
* Constructor for subclass builders that will call
* {@link #transport(GraphQlTransport)} to set the transport instance
* before {@link #build()}.
* Default constructor for use from subclasses.
* <p>Subclasses must set the transport to use before {@link #build()} or
* during, by overriding {@link #build()}.
*/
DefaultGraphQlClientBuilder() {
protected AbstractGraphQlClientBuilder() {
}
protected void transport(GraphQlTransport transport) {
this.transport = transport;
}
@Override
public B documentSource(@Nullable DocumentSource contentLoader) {
public B documentSource(DocumentSource contentLoader) {
this.documentSource = contentLoader;
return self();
}
@@ -88,10 +78,13 @@ public class DefaultGraphQlClientBuilder<B extends DefaultGraphQlClientBuilder<B
return (T) this;
}
@Override
public GraphQlClient build() {
Assert.notNull(this.transport, "No GraphQlTransport. Has a subclass not initialized it?");
return new DefaultGraphQlClient(this.transport, initJsonPathConfig(), initDocumentSource(), getBuilderInitializer());
/**
* Subclasses call this from {@link #build()} to provide the transport and get
* the default {@code GraphQlClient to delegate to for request execution.
*/
protected GraphQlClient buildGraphQlClient(GraphQlTransport transport) {
Assert.notNull(transport, "GraphQlTransport is required");
return new DefaultGraphQlClient(transport, initJsonPathConfig(), initDocumentSource(), getBuilderInitializer());
}
private Configuration initJsonPathConfig() {
@@ -105,8 +98,8 @@ public class DefaultGraphQlClientBuilder<B extends DefaultGraphQlClientBuilder<B
}
/**
* Exposes a {@code Consumer} to subclasses to initialize new builder instances
* from the configuration of "this" builder.
* Subclasses call this from {@link #build()} to obtain a {@code Consumer} to
* initialize new builder instances with, based on "this" builder.
*/
protected Consumer<GraphQlClient.Builder<?>> getBuilderInitializer() {
return builder -> {
@@ -114,7 +107,6 @@ public class DefaultGraphQlClientBuilder<B extends DefaultGraphQlClientBuilder<B
builder.documentSource(documentSource);
}
};
}

View File

@@ -36,11 +36,7 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Default {@link GraphQlClient} implementation with the logic to initialize
* requests and handle responses, and delegates to a {@link GraphQlTransport}
* for actual request execution.
*
* <p>This class is final but works with any transport.
* Default, final {@link GraphQlClient} implementation for use with any transport.
*
* @author Rossen Stoyanchev
* @since 1.0.0
@@ -53,16 +49,17 @@ final class DefaultGraphQlClient implements GraphQlClient {
private final DocumentSource documentSource;
private final Consumer<Builder<?>> builderInitializer;
private final Consumer<GraphQlClient.Builder<?>> builderInitializer;
DefaultGraphQlClient(
GraphQlTransport transport, Configuration jsonPathConfig, DocumentSource documentSource,
Consumer<Builder<?>> builderInitializer) {
Consumer<GraphQlClient.Builder<?>> builderInitializer) {
Assert.notNull(transport, "GraphQlTransport is required");
Assert.notNull(jsonPathConfig, "Configuration is required");
Assert.notNull(jsonPathConfig, "JSONPath Configuration is required");
Assert.notNull(documentSource, "DocumentSource is required");
Assert.notNull(documentSource, "`builderInitializer` is required");
this.transport = transport;
this.jsonPathConfig = jsonPathConfig;
@@ -83,13 +80,33 @@ final class DefaultGraphQlClient implements GraphQlClient {
}
@Override
public Builder<?> mutate() {
DefaultGraphQlClientBuilder<?> builder = new DefaultGraphQlClientBuilder<>(this.transport);
public Builder mutate() {
Builder builder = new Builder(this.transport);
this.builderInitializer.accept(builder);
return builder;
}
/**
* Default {@link GraphQlClient.Builder} with a given transport.
*/
static final class Builder extends AbstractGraphQlClientBuilder<Builder> {
private final GraphQlTransport transport;
Builder(GraphQlTransport transport) {
Assert.notNull(transport, "GraphQlTransport is required");
this.transport = transport;
}
@Override
public GraphQlClient build() {
return super.buildGraphQlClient(this.transport);
}
}
private static final class DefaultRequestSpec implements RequestSpec {
private final Mono<String> documentMono;

View File

@@ -17,188 +17,122 @@
package org.springframework.graphql.client;
import java.net.URI;
import java.util.Arrays;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.lang.Nullable;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriBuilderFactory;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Default {@link HttpGraphQlClient} implementation.
* Default {@link HttpGraphQlClient} implementation that builds the underlying
* {@code HttpGraphQlTransport} to use.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
final class DefaultHttpGraphQlClient extends AbstractDelegatingGraphQlClient implements HttpGraphQlClient {
private final Supplier<Builder> mutateBuilder;
private final WebClient webClient;
private final Consumer<GraphQlClient.Builder<?>> builderInitializer;
DefaultHttpGraphQlClient(GraphQlClient graphQlClient, Supplier<Builder> mutateBuilder) {
DefaultHttpGraphQlClient(GraphQlClient graphQlClient, WebClient webClient,
Consumer<GraphQlClient.Builder<?>> builderInitializer) {
super(graphQlClient);
this.mutateBuilder = mutateBuilder;
Assert.notNull(webClient, "WebClient is required");
Assert.notNull(builderInitializer, "`builderInitializer` is required");
this.webClient = webClient;
this.builderInitializer = builderInitializer;
}
public Builder mutate() {
return this.mutateBuilder.get();
}
static class BaseBuilder<B extends BaseBuilder<B>> extends DefaultGraphQlClientBuilder<B>
implements HttpGraphQlClient.BaseBuilder<B> {
@Nullable
private URI url;
private final HttpHeaders headers = new HttpHeaders();
@Nullable
private Consumer<ClientCodecConfigurer> codecConfigurerConsumer;
@Override
public B url(@Nullable String url) {
this.url = (url != null ? URI.create(url) : null);
return self();
}
@Override
public B url(@Nullable URI url) {
this.url = url;
return self();
}
@Override
public B header(String name, String... values) {
Arrays.stream(values).forEach(value -> this.headers.add(name, value));
return self();
}
@Override
public B headers(Consumer<HttpHeaders> headersConsumer) {
headersConsumer.accept(this.headers);
return self();
}
@Override
public B codecConfigurer(Consumer<ClientCodecConfigurer> codecConsumer) {
this.codecConfigurerConsumer = codecConsumer;
return self();
}
@Nullable
protected URI getUrl() {
return this.url;
}
protected HttpHeaders getHeaders() {
return this.headers;
}
@Nullable
protected Consumer<ClientCodecConfigurer> getCodecConfigurerConsumer() {
return this.codecConfigurerConsumer;
}
@SuppressWarnings("unchecked")
private <T extends B> T self() {
return (T) this;
}
/**
* Exposes a {@code Consumer} to subclasses to initialize new builder instances
* from the configuration of "this" builder.
*/
protected Consumer<HttpGraphQlClient.BaseBuilder<?>> getWebBuilderInitializer() {
Consumer<GraphQlClient.Builder<?>> parentInitializer = getBuilderInitializer();
HttpHeaders headersCopy = new HttpHeaders();
headersCopy.putAll(getHeaders());
return builder -> {
builder.url(getUrl()).headers(headers -> headers.putAll(headersCopy));
if (getCodecConfigurerConsumer() != null) {
builder.codecConfigurer(getCodecConfigurerConsumer());
}
parentInitializer.accept(builder);
};
}
Builder builder = new Builder(this.webClient);
this.builderInitializer.accept(builder);
return builder;
}
/**
* Default {@link HttpGraphQlClient.Builder} implementation.
* Default {@link HttpGraphQlClient.Builder} implementation, simply wrapping
* and delegating to {@link WebClient.Builder}.
*/
static final class Builder extends BaseBuilder<Builder> implements HttpGraphQlClient.Builder<Builder> {
@Nullable
private WebClient webClient;
@Nullable
private Consumer<WebClient.Builder> webClientConfigurers;
static final class Builder extends AbstractGraphQlClientBuilder<Builder> implements HttpGraphQlClient.Builder<Builder> {
private final WebClient.Builder webClientBuilder;
/**
* Constructor to start without a WebClient instance.
*/
Builder() {
this(WebClient.builder());
}
/**
* Constructor to start with a pre-configured {@code WebClient}.
*/
Builder(WebClient webClient) {
this.webClient = webClient;
Builder(WebClient client) {
this(client.mutate());
}
/**
* Constructor to start with a pre-configured {@code WebClient}.
*/
Builder(WebClient.Builder clientBuilder) {
this.webClientBuilder = clientBuilder;
}
@Override
public Builder url(String url) {
this.webClientBuilder.baseUrl(url);
return this;
}
@Override
public Builder url(URI url) {
UriBuilderFactory factory = new DefaultUriBuilderFactory(UriComponentsBuilder.fromUri(url));
this.webClientBuilder.uriBuilderFactory(factory);
return this;
}
@Override
public Builder header(String name, String... values) {
this.webClientBuilder.defaultHeader(name, values);
return this;
}
@Override
public Builder headers(Consumer<HttpHeaders> headersConsumer) {
this.webClientBuilder.defaultHeaders(headersConsumer);
return this;
}
@Override
public Builder codecConfigurer(Consumer<CodecConfigurer> codecsConsumer) {
this.webClientBuilder.codecs(codecsConsumer::accept);
return this;
}
@Override
public Builder webClient(Consumer<WebClient.Builder> configurer) {
this.webClientConfigurers = (this.webClientConfigurers != null ? this.webClientConfigurers.andThen(configurer) : configurer);
configurer.accept(this.webClientBuilder);
return this;
}
@Override
public HttpGraphQlClient build() {
WebClient webClient = initWebClient();
HttpGraphQlTransport transport = new HttpGraphQlTransport(webClient);
transport(transport);
GraphQlClient graphQlClient = super.build();
return new DefaultHttpGraphQlClient(graphQlClient, initMutateBuilderFactory(webClient));
}
private WebClient initWebClient() {
WebClient.Builder builder = (this.webClient != null ? this.webClient.mutate() : WebClient.builder());
if (getUrl() != null) {
builder.baseUrl(getUrl().toASCIIString());
}
builder.defaultHeaders(headers -> headers.putAll(getHeaders()));
if (getCodecConfigurerConsumer() != null) {
builder.codecs(getCodecConfigurerConsumer());
}
if (this.webClientConfigurers != null) {
this.webClientConfigurers.accept(builder);
}
return builder.build();
}
private Supplier<Builder> initMutateBuilderFactory(WebClient webClient) {
Consumer<HttpGraphQlClient.BaseBuilder<?>> parentInitializer = getWebBuilderInitializer();
return () -> {
Builder builder = new Builder(webClient);
parentInitializer.accept(builder);
return builder;
};
WebClient webClient = this.webClientBuilder.build();
GraphQlClient graphQlClient = super.buildGraphQlClient(new HttpGraphQlTransport(webClient));
return new DefaultHttpGraphQlClient(graphQlClient, webClient, getBuilderInitializer());
}
}

View File

@@ -16,20 +16,23 @@
package org.springframework.graphql.client;
import java.util.Map;
import java.net.URI;
import java.util.Arrays;
import java.util.function.Consumer;
import java.util.function.Supplier;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.lang.Nullable;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.util.Assert;
import org.springframework.web.reactive.socket.client.WebSocketClient;
import org.springframework.web.util.DefaultUriBuilderFactory;
/**
* Default {@link WebSocketGraphQlClient} implementation.
* Default {@link WebSocketGraphQlClient} implementation that builds the underlying
* {@code WebSocketGraphQlTransport} to use.
*
* @author Rossen Stoyanchev
* @since 1.0.0
@@ -38,15 +41,19 @@ final class DefaultWebSocketGraphQlClient extends AbstractDelegatingGraphQlClien
private final WebSocketGraphQlTransport transport;
private final Supplier<Builder> mutateBuilderFactory;
private final Consumer<GraphQlClient.Builder<?>> builderInitializer;
DefaultWebSocketGraphQlClient(
GraphQlClient delegate, WebSocketGraphQlTransport transport, Supplier<Builder> mutateBuilderFactory) {
DefaultWebSocketGraphQlClient(GraphQlClient delegate, WebSocketGraphQlTransport transport,
Consumer<GraphQlClient.Builder<?>> builderInitializer) {
super(delegate);
Assert.notNull(transport, "WebSocketGraphQlTransport is required");
Assert.notNull(builderInitializer, "`builderInitializer` is required");
this.transport = transport;
this.mutateBuilderFactory = mutateBuilderFactory;
this.builderInitializer = builderInitializer;
}
@@ -62,72 +69,84 @@ final class DefaultWebSocketGraphQlClient extends AbstractDelegatingGraphQlClien
@Override
public Builder mutate() {
return this.mutateBuilderFactory.get();
Builder builder = new Builder(this.transport);
this.builderInitializer.accept(builder);
return builder;
}
/**
* Default {@link WebSocketGraphQlClient.Builder} implementation.
*/
static final class Builder extends DefaultHttpGraphQlClient.BaseBuilder<Builder>
static final class Builder extends AbstractGraphQlClientBuilder<Builder>
implements WebSocketGraphQlClient.Builder<Builder> {
private URI url;
private final HttpHeaders headers = new HttpHeaders();
private final WebSocketClient webSocketClient;
@Nullable
private Object initPayload;
private final CodecConfigurer codecConfigurer;
private Consumer<Map<String, Object>> connectionAckHandler = ackPayload -> {};
Builder(WebSocketClient client) {
/**
* Constructor to start via {@link WebSocketGraphQlClient#builder(URI, WebSocketClient)}.
*/
Builder(URI url, WebSocketClient client) {
this.url = url;
this.webSocketClient = client;
this.codecConfigurer = ClientCodecConfigurer.create();
}
/**
* Constructor to mutate.
* @param transport the underlying transport with the current state
*/
Builder(WebSocketGraphQlTransport transport) {
this.url = transport.getUrl();
this.headers.putAll(transport.getHeaders());
this.webSocketClient = transport.getWebSocketClient();
this.codecConfigurer = transport.getCodecConfigurer();
}
@Override
public Builder connectionInitPayload(@Nullable Object connectionInitPayload) {
this.initPayload = connectionInitPayload;
public Builder url(String url) {
url(new DefaultUriBuilderFactory().uriString(url).build());
return this;
}
@Override
public Builder connectionAckHandler(Consumer<Map<String, Object>> ackHandler) {
this.connectionAckHandler = ackHandler;
public Builder url(URI url) {
this.url = url;
return this;
}
@Override
public Builder header(String name, String... values) {
this.headers.put(name, Arrays.asList(values));
return this;
}
@Override
public Builder headers(Consumer<HttpHeaders> headersConsumer) {
headersConsumer.accept(this.headers);
return this;
}
@Override
public Builder codecConfigurer(Consumer<CodecConfigurer> codecConsumer) {
codecConsumer.accept(this.codecConfigurer);
return this;
}
@Override
public WebSocketGraphQlClient build() {
Assert.notNull(getUrl(), "GraphQL endpoint URI is required");
WebSocketGraphQlTransport transport = new WebSocketGraphQlTransport(
getUrl(), getHeaders(), this.webSocketClient, initClientCodecConfigurer(),
this.initPayload, this.connectionAckHandler);
this.url, this.headers, this.webSocketClient, this.codecConfigurer, null, payload -> {});
transport(transport);
GraphQlClient graphQlClient = super.build();
return new DefaultWebSocketGraphQlClient(graphQlClient, transport, mutateBuilderFactory());
}
private ClientCodecConfigurer initClientCodecConfigurer() {
ClientCodecConfigurer configurer = ClientCodecConfigurer.create();
if (getCodecConfigurerConsumer() != null) {
getCodecConfigurerConsumer().accept(configurer);
}
return configurer;
}
private Supplier<Builder> mutateBuilderFactory() {
Consumer<HttpGraphQlClient.BaseBuilder<?>> parentBuilderInitializer = getWebBuilderInitializer();
return () -> {
Builder builder = new Builder(this.webSocketClient);
builder.connectionInitPayload(this.initPayload);
builder.connectionAckHandler(this.connectionAckHandler);
parentBuilderInitializer.accept(builder);
return builder;
};
GraphQlClient graphQlClient = super.buildGraphQlClient(transport);
return new DefaultWebSocketGraphQlClient(graphQlClient, transport, getBuilderInitializer());
}
}

View File

@@ -29,16 +29,18 @@ import org.springframework.graphql.support.ResourceDocumentSource;
import org.springframework.lang.Nullable;
/**
* Defines workflow to execute GraphQL requests, independent of the transport.
* Define a workflow to execute GraphQL requests that is independent of the
* underlying transport.
*
* <p>In most cases, you'll want to use a transport specific extension:
* <p>For most cases, use a transport specific extension:
* <ul>
* <li>{@link HttpGraphQlClient}
* <li>{@link WebSocketGraphQlClient}
* </ul>
*
* <p>Alternatively, use {@link #builder(GraphQlTransport)} to create an instance
* with any other transport. Or create a transport specific extension.
* <p>Alternatively, create an instance with any other transport via
* {@link #builder(GraphQlTransport)}. Or create a transport specific extension
* similar to HTTP and WebSocket.
*
* @author Rossen Stoyanchev
* @since 1.0.0
@@ -70,15 +72,15 @@ public interface GraphQlClient {
/**
* Create a builder with the given {@code GraphQlTransport}.
* <p>For GraphQL over HTTP and WebSocket, consider using the extensions
* {@link HttpGraphQlClient} and {@link WebSocketGraphQlClient}.
* This allows plugging in any other transport implementation.
* Create a builder with the given custom {@code GraphQlTransport}.
* <p>For most cases, use a transport specific extension such as
* {@link HttpGraphQlClient} or {@link WebSocketGraphQlClient}. This method
* is for use with a custom {@code GraphQlTransport}.
* @param transport the transport to execute requests with
* @return the builder for further initialization
*/
static Builder<?> builder(GraphQlTransport transport) {
return new DefaultGraphQlClientBuilder<>(transport);
return new DefaultGraphQlClient.Builder(transport);
}
@@ -92,7 +94,7 @@ public interface GraphQlClient {
* {@link #documentName(String)} for resolving a document by name.
* <p>By default, {@link ResourceDocumentSource} is used.
*/
B documentSource(@Nullable DocumentSource contentLoader);
B documentSource(DocumentSource contentLoader);
/**
* Build the {@code GraphQlClient} instance.

View File

@@ -24,7 +24,7 @@ import org.springframework.graphql.GraphQlRequest;
/**
* Contract for GraphQL request execution over some transport.
* Contract for executing GraphQL requests over some transport.
*
* @author Rossen Stoyanchev
* @since 1.0.0
@@ -32,20 +32,18 @@ import org.springframework.graphql.GraphQlRequest;
public interface GraphQlTransport {
/**
* Execute a request that returns a single response such as a "query" or a
* "mutation" operation.
* Execute a request with a single response such as a "query" or "mutation".
* @param request the request to execute
* @return a {@code Mono} with the {@code ExecutionResult} for the response.
* The {@code Mono} may end wth an error due to transport or other issues
* such as failures to encode the request or decode the response.
* </ul>
*/
Mono<ExecutionResult> execute(GraphQlRequest request);
/**
* Execute a "subscription" request that returns a stream of responses.
* Execute a "subscription" request with a stream of responses.
* @param request the request to execute
* @return a {@code Flux} with an {@code ExecutionResult} for each response.
* @return a {@code Flux} of {@code ExecutionResult} responses.
* The {@code Flux} may terminate as follows:
* <ul>
* <li>Completes if the subscription completes before the connection is closed.

View File

@@ -16,22 +16,18 @@
package org.springframework.graphql.client;
import java.net.URI;
import java.util.function.Consumer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.lang.Nullable;
import org.springframework.web.reactive.function.client.WebClient;
/**
* {@code GraphQlClient} for GraphQL over HTTP via {@link WebClient}.
* GraphQL over HTTP client that uses {@link WebClient}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public interface HttpGraphQlClient extends GraphQlClient {
public interface HttpGraphQlClient extends WebGraphQlClient {
@Override
@@ -42,7 +38,7 @@ public interface HttpGraphQlClient extends GraphQlClient {
* Create an {@link HttpGraphQlClient} that uses the given {@link WebClient}.
*/
static HttpGraphQlClient create(WebClient webClient) {
return builder(webClient).build();
return builder(webClient.mutate()).build();
}
/**
@@ -54,65 +50,30 @@ public interface HttpGraphQlClient extends GraphQlClient {
/**
* Variant of {@link #builder()} with a pre-configured {@code WebClient}
* which may be mutated and further customized through the returned builder.
* to mutate and customize further through the returned builder.
*/
static Builder<?> builder(WebClient webClient) {
return new DefaultHttpGraphQlClient.Builder(webClient);
static Builder<?> builder(WebClient.Builder webClientBuilder) {
return new DefaultHttpGraphQlClient.Builder(webClientBuilder);
}
/**
* Base builder for GraphQL clients over a Web transport.
* Builder for the GraphQL over HTTP client.
*/
interface BaseBuilder<B extends BaseBuilder<B>> extends GraphQlClient.Builder<B> {
/**
* Set the GraphQL endpoint URL.
* @param url the url to make requests to
*/
B url(@Nullable String url);
/**
* Set the GraphQL endpoint URL.
* @param url the url to make requests to
*/
B url(@Nullable URI url);
/**
* Add the given header to HTTP requests to the endpoint URL.
* @param name the header name
* @param values the header values
*/
B header(String name, String... values);
/**
* Variant of {@link #header(String, String...)} that provides access
* to the underlying headers to inspect or modify directly.
* @param headersConsumer a function that consumes the {@code HttpHeaders}
*/
B headers(Consumer<HttpHeaders> headersConsumer);
/**
* Provide a {@code Consumer} to customize the {@code ClientCodecConfigurer}
* for JSON encoding and decoding of GraphQL payloads.
*/
B codecConfigurer(Consumer<ClientCodecConfigurer> codecsConsumer);
}
/**
* Builder for a GraphQL over HTTP client.
*/
interface Builder<B extends Builder<B>> extends BaseBuilder<B> {
interface Builder<B extends Builder<B>> extends WebGraphQlClient.Builder<B> {
/**
* Customize the {@code WebClient} to use.
* <p>Note that some properties of {@code WebClient.Builder} like the
* base URL, headers, and codecs can be customized through this builder.
* @see #url(String)
* @see #header(String, String...)
* @see #codecConfigurer(Consumer)
*/
B webClient(Consumer<WebClient.Builder> webClient);
/**
* Build the {@code HttpGraphQlClient}.
* Build the {@code HttpGraphQlClient} instance.
*/
@Override
HttpGraphQlClient build();

View File

@@ -29,6 +29,7 @@ import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Transport to execute GraphQL requests over HTTP via {@link WebClient}.
* Supports only single-response requests over HTTP POST. For subscription

View File

@@ -0,0 +1,85 @@
/*
* 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.net.URI;
import java.util.function.Consumer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.CodecConfigurer;
/**
* Base contract for the HTTP and WebSocket {@code GraphQlClient} extensions.
* Defines a builder with common configuration for both transports.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public interface WebGraphQlClient extends GraphQlClient {
@Override
Builder<?> mutate();
/**
* Base builder for GraphQL clients over a Web transport.
*/
interface Builder<B extends Builder<B>> extends GraphQlClient.Builder<B> {
/**
* Set the GraphQL endpoint URL as a String.
* @param url the url to send HTTP requests to or connect over WebSocket
*/
B url(String url);
/**
* Set the GraphQL endpoint URL.
* @param url the url to send HTTP requests to or connect over WebSocket
*/
B url(URI url);
/**
* Add the given header to HTTP requests or to the WebSocket handshake request.
* @param name the header name
* @param values the header values
*/
B header(String name, String... values);
/**
* Variant of {@link #header(String, String...)} that provides access
* to the underlying headers to inspect or modify directly.
* @param headersConsumer a function that consumes the {@code HttpHeaders}
*/
B headers(Consumer<HttpHeaders> headersConsumer);
/**
* Configure the underlying {@code CodecConfigurer} to use for all JSON
* encoding and decoding needs.
*/
B codecConfigurer(Consumer<CodecConfigurer> codecsConsumer);
/**
* Build a {@code WebGraphQlClient} instance.
*/
@Override
WebGraphQlClient build();
}
}

View File

@@ -43,6 +43,8 @@ final class WebSocketCodecDelegate {
private static final ResolvableType MESSAGE_TYPE = ResolvableType.forClass(GraphQlWebSocketMessage.class);
private final CodecConfigurer configurer;
private final Decoder<?> decoder;
private final Encoder<?> encoder;
@@ -52,10 +54,11 @@ final class WebSocketCodecDelegate {
this(ClientCodecConfigurer.create());
}
WebSocketCodecDelegate(CodecConfigurer codecConfigurer) {
Assert.notNull(codecConfigurer, "CodecConfigurer is required");
this.decoder = initDecoder(codecConfigurer);
this.encoder = initEncoder(codecConfigurer);
WebSocketCodecDelegate(CodecConfigurer configurer) {
Assert.notNull(configurer, "CodecConfigurer is required");
this.configurer = configurer;
this.decoder = initDecoder(configurer);
this.encoder = initEncoder(configurer);
}
private static Decoder<?> initDecoder(CodecConfigurer configurer) {
@@ -75,6 +78,11 @@ final class WebSocketCodecDelegate {
}
public CodecConfigurer getCodecConfigurer() {
return this.configurer;
}
@SuppressWarnings("unchecked")
public <T> WebSocketMessage encode(WebSocketSession session, GraphQlWebSocketMessage message) {

View File

@@ -17,33 +17,32 @@
package org.springframework.graphql.client;
import java.net.URI;
import java.util.Map;
import java.util.function.Consumer;
import reactor.core.publisher.Mono;
import org.springframework.lang.Nullable;
import org.springframework.web.reactive.socket.client.WebSocketClient;
/**
* {@code GraphQlClient} for GraphQL over Web via {@link WebSocketClient}.
* GraphQL over WebSocket client that uses {@link WebSocketClient}.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public interface WebSocketGraphQlClient extends GraphQlClient {
public interface WebSocketGraphQlClient extends WebGraphQlClient {
/**
* Start the transport by connecting the WebSocket, sending the
* "connection_init" and waiting for the "connection_ack" message.
* Start the GraphQL session by connecting the WebSocket, sending the
* "connection_init" and receiving the "connection_ack" message.
* <p><strong>Note:</Strong> Only one session is started at a time.
* Additional attempts to start have no impact while a session is active.
* @return {@code Mono} that completes when the WebSocket is connected and
* ready to begin sending GraphQL requests
* the GraphQL session is ready to send requests
*/
Mono<Void> start();
/**
* Stop the transport by closing the WebSocket with
* Stop the GraphQL session by closing the WebSocket with
* {@link org.springframework.web.reactive.socket.CloseStatus#NORMAL} and
* terminating in-progress requests with an error signal.
* <p>New requests are rejected from the time of this call. If necessary,
@@ -57,38 +56,28 @@ public interface WebSocketGraphQlClient extends GraphQlClient {
/**
* Create a {@link WebSocketGraphQlClient} that uses the given
* {@code WebSocketClient} to connect to the given URL.
* Create a {@link WebSocketGraphQlClient}.
* @param url the GraphQL endpoint URL
* @param webSocketClient the transport client to use
* @param webSocketClient the underlying transport client to use
*/
static WebSocketGraphQlClient create(URI url, WebSocketClient webSocketClient) {
return builder(webSocketClient).url(url).build();
return builder(url, webSocketClient).build();
}
/**
* Return a builder to initialize a {@link WebSocketGraphQlClient} with.
* @param webSocketClient the transport client to use
* Return a builder for a {@link WebSocketGraphQlClient}.
* @param url the GraphQL endpoint URL
* @param webSocketClient the underlying transport client to use
*/
static Builder<?> builder(WebSocketClient webSocketClient) {
return new DefaultWebSocketGraphQlClient.Builder(webSocketClient);
static Builder<?> builder(URI url, WebSocketClient webSocketClient) {
return new DefaultWebSocketGraphQlClient.Builder(url, webSocketClient);
}
/**
* Builder for a GraphQL over WebSocket client.
*/
interface Builder<B extends Builder<B>> extends HttpGraphQlClient.BaseBuilder<B> {
/**
* The payload to send with the "connection_init" message.
*/
B connectionInitPayload(@Nullable Object connectionInitPayload);
/**
* Handler for the payload received with the "connection_ack" message.
*/
B connectionAckHandler(Consumer<Map<String, Object>> ackHandler);
interface Builder<B extends Builder<B>> extends WebGraphQlClient.Builder<B> {
/**
* Build the {@code WebSocketGraphQlClient}.

View File

@@ -57,6 +57,11 @@ final class WebSocketGraphQlTransport implements GraphQlTransport {
private static final Log logger = LogFactory.getLog(WebSocketGraphQlTransport.class);
private final URI url;
private final HttpHeaders headers = new HttpHeaders();
private final WebSocketClient webSocketClient;
private final GraphQlSessionHandler graphQlSessionHandler;
@@ -64,13 +69,20 @@ final class WebSocketGraphQlTransport implements GraphQlTransport {
WebSocketGraphQlTransport(
URI uri, HttpHeaders headers, WebSocketClient client, CodecConfigurer codecConfigurer,
URI url, @Nullable HttpHeaders headers, WebSocketClient client, CodecConfigurer codecConfigurer,
@Nullable Object connectionInitPayload, Consumer<Map<String, Object>> connectionAckHandler) {
Assert.notNull(url, "URI is required");
Assert.notNull(url, "URI is required");
this.url = url;
this.headers.putAll(headers != null ? headers : HttpHeaders.EMPTY);
this.webSocketClient = client;
this.graphQlSessionHandler = new GraphQlSessionHandler(
codecConfigurer, connectionInitPayload, connectionAckHandler);
this.graphQlSessionMono = initGraphQlSession(uri, headers, client, this.graphQlSessionHandler)
this.graphQlSessionMono = initGraphQlSession(this.url, this.headers, client, this.graphQlSessionHandler)
.cacheInvalidateWhen(GraphQlSession::notifyWhenClosed);
}
@@ -90,6 +102,23 @@ final class WebSocketGraphQlTransport implements GraphQlTransport {
}
public URI getUrl() {
return this.url;
}
public HttpHeaders getHeaders() {
return this.headers;
}
public WebSocketClient getWebSocketClient() {
return this.webSocketClient;
}
public CodecConfigurer getCodecConfigurer() {
return this.graphQlSessionHandler.getCodecConfigurer();
}
/**
* Start the transport by connecting the WebSocket, sending the
* "connection_init" and waiting for the "connection_ack" message.
@@ -158,6 +187,11 @@ final class WebSocketGraphQlTransport implements GraphQlTransport {
}
public CodecConfigurer getCodecConfigurer() {
return this.codecDelegate.getCodecConfigurer();
}
@Override
public List<String> getSubProtocols() {
return Collections.singletonList("graphql-transport-ws");

View File

@@ -15,7 +15,8 @@
*/
/**
* GraphQL client.
* This package contains a {@link org.springframework.graphql.client.GraphQlClient}
* along with HTTP and WebSocket extensions.
*/
@NonNullApi
@NonNullFields