Add transport specific GraphQlClient extensions
Add HttpGraphQlClient and WebSocketGraphQlClient extensions along with a builder hierarchy that combines GraphQlClient and transport specific configuration. See gh-10
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 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.
|
||||
*
|
||||
* <p>Subclasses must implement {@link GraphQlClient#mutate()} to allow mutation
|
||||
* of both {@code GraphQlClient} and {@code GraphQlTransport} configuration.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public abstract class AbstractDelegatingGraphQlClient implements GraphQlClient {
|
||||
|
||||
private final GraphQlClient graphQlClient;
|
||||
|
||||
|
||||
protected AbstractDelegatingGraphQlClient(GraphQlClient graphQlClient) {
|
||||
Assert.notNull(graphQlClient, "GraphQlClient is required");
|
||||
this.graphQlClient = graphQlClient;
|
||||
}
|
||||
|
||||
|
||||
public GraphQlClient.RequestSpec document(String document) {
|
||||
return this.graphQlClient.document(document);
|
||||
}
|
||||
|
||||
public GraphQlClient.RequestSpec documentName(String name) {
|
||||
return this.graphQlClient.documentName(name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package org.springframework.graphql.client;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
@@ -35,7 +36,11 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link GraphQlClient}.
|
||||
* 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.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
@@ -48,9 +53,12 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
private final DocumentSource documentSource;
|
||||
|
||||
private final Consumer<Builder<?>> builderInitializer;
|
||||
|
||||
|
||||
DefaultGraphQlClient(
|
||||
GraphQlTransport transport, Configuration jsonPathConfig, DocumentSource documentSource) {
|
||||
GraphQlTransport transport, Configuration jsonPathConfig, DocumentSource documentSource,
|
||||
Consumer<Builder<?>> builderInitializer) {
|
||||
|
||||
Assert.notNull(transport, "GraphQlTransport is required");
|
||||
Assert.notNull(jsonPathConfig, "Configuration is required");
|
||||
@@ -59,15 +67,10 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
this.transport = transport;
|
||||
this.jsonPathConfig = jsonPathConfig;
|
||||
this.documentSource = documentSource;
|
||||
this.builderInitializer = builderInitializer;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T extends GraphQlTransport> T getTransport(Class<T> requiredType) {
|
||||
return (requiredType.isInstance(this.transport) ? (T) this.transport : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestSpec document(String document) {
|
||||
return new DefaultRequestSpec(Mono.just(document), this.transport, this.jsonPathConfig);
|
||||
@@ -79,6 +82,13 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
return new DefaultRequestSpec(document, this.transport, this.jsonPathConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder<?> mutate() {
|
||||
DefaultGraphQlClientBuilder<?> builder = new DefaultGraphQlClientBuilder<>(this.transport);
|
||||
this.builderInitializer.accept(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
private static final class DefaultRequestSpec implements RequestSpec {
|
||||
|
||||
@@ -112,6 +122,12 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestSpec variables(Map<String, Object> variables) {
|
||||
this.variables.putAll(variables);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseSpec> execute() {
|
||||
return getRequestMono()
|
||||
@@ -136,11 +152,14 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
private static class DefaultResponseSpec implements ResponseSpec {
|
||||
|
||||
private final ExecutionResult result;
|
||||
|
||||
private final DocumentContext jsonPathDocument;
|
||||
|
||||
private final List<GraphQLError> errors;
|
||||
|
||||
private DefaultResponseSpec(ExecutionResult result, Configuration jsonPathConfig) {
|
||||
this.result = result;
|
||||
this.jsonPathDocument = JsonPath.parse(result.toSpecification(), jsonPathConfig);
|
||||
this.errors = result.getErrors();
|
||||
}
|
||||
@@ -180,6 +199,11 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutionResult andReturn() {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,25 +13,34 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
|
||||
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@link GraphQlClient.Builder}.
|
||||
* Default {@link GraphQlClient.Builder} implementation that builds a
|
||||
* {@link GraphQlClient} for use with any transport.
|
||||
*
|
||||
* <p>Intended for use as a base class for builders that do assist with building
|
||||
* the underlying transport. Such extension
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class DefaultGraphQlClientBuilder implements GraphQlClient.Builder {
|
||||
public class DefaultGraphQlClientBuilder<B extends DefaultGraphQlClientBuilder<B>> implements GraphQlClient.Builder<B> {
|
||||
|
||||
private static final boolean jackson2Present;
|
||||
|
||||
@@ -42,53 +51,70 @@ class DefaultGraphQlClientBuilder implements GraphQlClient.Builder {
|
||||
}
|
||||
|
||||
|
||||
private final GraphQlTransport transport;
|
||||
|
||||
@Nullable
|
||||
private Configuration jsonPathConfig;
|
||||
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()}.
|
||||
*/
|
||||
DefaultGraphQlClientBuilder() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlClient.Builder jsonPathConfig(@Nullable Configuration config) {
|
||||
this.jsonPathConfig = config;
|
||||
return this;
|
||||
protected void transport(GraphQlTransport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlClient.Builder documentSource(@Nullable DocumentSource contentLoader) {
|
||||
public B documentSource(@Nullable DocumentSource contentLoader) {
|
||||
this.documentSource = contentLoader;
|
||||
return this;
|
||||
return self();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends B> T self() {
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlClient build() {
|
||||
return new DefaultGraphQlClient(this.transport, initJsonPathConfig(), initRequestNameResolver());
|
||||
Assert.notNull(this.transport, "No GraphQlTransport. Has a subclass not initialized it?");
|
||||
return new DefaultGraphQlClient(this.transport, initJsonPathConfig(), initDocumentSource(), getBuilderInitializer());
|
||||
}
|
||||
|
||||
private Configuration initJsonPathConfig() {
|
||||
if (this.jsonPathConfig != null) {
|
||||
return this.jsonPathConfig;
|
||||
}
|
||||
else if (jackson2Present) {
|
||||
return Jackson2Configuration.create();
|
||||
}
|
||||
else {
|
||||
return Configuration.builder().build();
|
||||
}
|
||||
// Allow configuring JSONPath with codecs from transport subclasses
|
||||
return (jackson2Present ? Jackson2Configuration.create() : Configuration.builder().build());
|
||||
}
|
||||
|
||||
private DocumentSource initRequestNameResolver() {
|
||||
private DocumentSource initDocumentSource() {
|
||||
return (this.documentSource == null ?
|
||||
new ResourceDocumentSource() : this.documentSource);
|
||||
new CachingDocumentSource(new ResourceDocumentSource()) : this.documentSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes a {@code Consumer} to subclasses to initialize new builder instances
|
||||
* from the configuration of "this" builder.
|
||||
*/
|
||||
protected Consumer<GraphQlClient.Builder<?>> getBuilderInitializer() {
|
||||
return builder -> {
|
||||
if (this.documentSource != null) {
|
||||
builder.documentSource(documentSource);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* 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.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.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* Default {@link HttpGraphQlClient} implementation.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class DefaultHttpGraphQlClient extends AbstractDelegatingGraphQlClient implements HttpGraphQlClient {
|
||||
|
||||
private final Supplier<Builder> mutateBuilder;
|
||||
|
||||
|
||||
DefaultHttpGraphQlClient(GraphQlClient graphQlClient, Supplier<Builder> mutateBuilder) {
|
||||
super(graphQlClient);
|
||||
this.mutateBuilder = mutateBuilder;
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default {@link HttpGraphQlClient.Builder} implementation.
|
||||
*/
|
||||
static final class Builder extends BaseBuilder<Builder> implements HttpGraphQlClient.Builder<Builder> {
|
||||
|
||||
@Nullable
|
||||
private WebClient webClient;
|
||||
|
||||
@Nullable
|
||||
private Consumer<WebClient.Builder> webClientConfigurers;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor to start without a WebClient instance.
|
||||
*/
|
||||
Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to start with a pre-configured {@code WebClient}.
|
||||
*/
|
||||
Builder(WebClient webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Builder webClient(Consumer<WebClient.Builder> configurer) {
|
||||
this.webClientConfigurers = (this.webClientConfigurers != null ? this.webClientConfigurers.andThen(configurer) : configurer);
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.codec.ClientCodecConfigurer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.socket.client.WebSocketClient;
|
||||
|
||||
|
||||
/**
|
||||
* Default {@link WebSocketGraphQlClient} implementation.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class DefaultWebSocketGraphQlClient extends AbstractDelegatingGraphQlClient implements WebSocketGraphQlClient {
|
||||
|
||||
private final WebSocketGraphQlTransport transport;
|
||||
|
||||
private final Supplier<Builder> mutateBuilderFactory;
|
||||
|
||||
|
||||
DefaultWebSocketGraphQlClient(
|
||||
GraphQlClient delegate, WebSocketGraphQlTransport transport, Supplier<Builder> mutateBuilderFactory) {
|
||||
|
||||
super(delegate);
|
||||
this.transport = transport;
|
||||
this.mutateBuilderFactory = mutateBuilderFactory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Void> start() {
|
||||
return this.transport.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> stop() {
|
||||
return this.transport.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder mutate() {
|
||||
return this.mutateBuilderFactory.get();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default {@link WebSocketGraphQlClient.Builder} implementation.
|
||||
*/
|
||||
static final class Builder extends DefaultHttpGraphQlClient.BaseBuilder<Builder>
|
||||
implements WebSocketGraphQlClient.Builder<Builder> {
|
||||
|
||||
private final WebSocketClient webSocketClient;
|
||||
|
||||
@Nullable
|
||||
private Object initPayload;
|
||||
|
||||
private Consumer<Map<String, Object>> connectionAckHandler = ackPayload -> {};
|
||||
|
||||
|
||||
Builder(WebSocketClient client) {
|
||||
this.webSocketClient = client;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Builder connectionInitPayload(@Nullable Object connectionInitPayload) {
|
||||
this.initPayload = connectionInitPayload;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder connectionAckHandler(Consumer<Map<String, Object>> ackHandler) {
|
||||
this.connectionAckHandler = ackHandler;
|
||||
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);
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,8 +16,9 @@
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQLError;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -28,29 +29,22 @@ import org.springframework.graphql.support.ResourceDocumentSource;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Defines a workflow to prepare and execute GraphQL requests and to decode and
|
||||
* handle responses.
|
||||
* Defines workflow to execute GraphQL requests, independent of the transport.
|
||||
*
|
||||
* <p>To create a {@link GraphQlClient}, use the builder in this class, and see
|
||||
* examples in {@link HttpGraphQlTransport} and {@link WebSocketGraphQlTransport}
|
||||
* for initializing both the client and the transport it runs over.
|
||||
* <p>In most cases, you'll want to 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.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
* @see HttpGraphQlTransport
|
||||
* @see WebSocketGraphQlTransport
|
||||
*/
|
||||
public interface GraphQlClient {
|
||||
|
||||
|
||||
/**
|
||||
* Return the underlying transport or {@code null} if the required type
|
||||
* does not match the transport type. See {@link GraphQlTransport}
|
||||
*/
|
||||
@Nullable
|
||||
<T extends GraphQlTransport> T getTransport(Class<T> requiredType);
|
||||
|
||||
|
||||
/**
|
||||
* Start defining a GraphQL request with the given document, which is the
|
||||
* textual representation of an operation (or operations) to perform,
|
||||
@@ -68,35 +62,37 @@ public interface GraphQlClient {
|
||||
*/
|
||||
RequestSpec documentName(String name);
|
||||
|
||||
/**
|
||||
* Return a builder initialized from the configuration of "this" client
|
||||
* to use to build a new, independently configured client instance.
|
||||
*/
|
||||
GraphQlClient.Builder<?> mutate();
|
||||
|
||||
|
||||
/**
|
||||
* Return a builder to initialize a {@link GraphQlClient} instance.
|
||||
* @param transport the transport for executing requests over
|
||||
* @see HttpGraphQlTransport
|
||||
* @see WebSocketGraphQlTransport
|
||||
* 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.
|
||||
* @param transport the transport to execute requests with
|
||||
* @return the builder for further initialization
|
||||
*/
|
||||
static Builder builder(GraphQlTransport transport) {
|
||||
return new DefaultGraphQlClientBuilder(transport);
|
||||
static Builder<?> builder(GraphQlTransport transport) {
|
||||
return new DefaultGraphQlClientBuilder<>(transport);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines a builder for creating {@link GraphQlClient} instances.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Provide JSONPath configuration settings.
|
||||
* <p>By default, the Jackson JSON library is used if present.
|
||||
*/
|
||||
Builder jsonPathConfig(@Nullable Configuration config);
|
||||
interface Builder<B extends Builder<B>> {
|
||||
|
||||
/**
|
||||
* Configure a {@link DocumentSource} for use with
|
||||
* {@link #documentName(String)} for resolving a document by name.
|
||||
* <p>By default, {@link ResourceDocumentSource} is used.
|
||||
*/
|
||||
Builder documentSource(@Nullable DocumentSource contentLoader);
|
||||
B documentSource(@Nullable DocumentSource contentLoader);
|
||||
|
||||
/**
|
||||
* Build the {@code GraphQlClient} instance.
|
||||
@@ -160,6 +156,13 @@ public interface GraphQlClient {
|
||||
*/
|
||||
RequestSpec variable(String name, Object value);
|
||||
|
||||
/**
|
||||
* Add all given values for variables defined by the operation.
|
||||
* @param variables the variable values
|
||||
* @return this request spec
|
||||
*/
|
||||
RequestSpec variables(Map<String, Object> variables);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -220,6 +223,11 @@ public interface GraphQlClient {
|
||||
*/
|
||||
List<GraphQLError> errors();
|
||||
|
||||
/**
|
||||
* Return the underlying {@link ExecutionResult} for the response.
|
||||
*/
|
||||
ExecutionResult andReturn();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.ClientCodecConfigurer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
|
||||
/**
|
||||
* {@code GraphQlClient} for GraphQL over HTTP via {@link WebClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface HttpGraphQlClient extends GraphQlClient {
|
||||
|
||||
|
||||
@Override
|
||||
Builder<?> mutate();
|
||||
|
||||
|
||||
/**
|
||||
* Create an {@link HttpGraphQlClient} that uses the given {@link WebClient}.
|
||||
*/
|
||||
static HttpGraphQlClient create(WebClient webClient) {
|
||||
return builder(webClient).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a builder to initialize an {@link HttpGraphQlClient} with.
|
||||
*/
|
||||
static Builder<?> builder() {
|
||||
return new DefaultHttpGraphQlClient.Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link #builder()} with a pre-configured {@code WebClient}
|
||||
* which may be mutated and further customized through the returned builder.
|
||||
*/
|
||||
static Builder<?> builder(WebClient webClient) {
|
||||
return new DefaultHttpGraphQlClient.Builder(webClient);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Base builder for GraphQL clients over a Web transport.
|
||||
*/
|
||||
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> {
|
||||
|
||||
/**
|
||||
* Customize the {@code WebClient} to use.
|
||||
*/
|
||||
B webClient(Consumer<WebClient.Builder> webClient);
|
||||
|
||||
/**
|
||||
* Build the {@code HttpGraphQlClient}.
|
||||
*/
|
||||
@Override
|
||||
HttpGraphQlClient build();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.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}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface WebSocketGraphQlClient extends GraphQlClient {
|
||||
|
||||
/**
|
||||
* Start the transport by connecting the WebSocket, sending the
|
||||
* "connection_init" and waiting for the "connection_ack" message.
|
||||
* @return {@code Mono} that completes when the WebSocket is connected and
|
||||
* ready to begin sending GraphQL requests
|
||||
*/
|
||||
Mono<Void> start();
|
||||
|
||||
/**
|
||||
* Stop the transport 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,
|
||||
* call {@link #start()} to allow requests again.
|
||||
* @return {@code Mono} that completes when the underlying session is closed
|
||||
*/
|
||||
Mono<Void> stop();
|
||||
|
||||
@Override
|
||||
Builder<?> mutate();
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@link WebSocketGraphQlClient} that uses the given
|
||||
* {@code WebSocketClient} to connect to the given URL.
|
||||
* @param url the GraphQL endpoint URL
|
||||
* @param webSocketClient the transport client to use
|
||||
*/
|
||||
static WebSocketGraphQlClient create(URI url, WebSocketClient webSocketClient) {
|
||||
return builder(webSocketClient).url(url).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a builder to initialize a {@link WebSocketGraphQlClient} with.
|
||||
* @param webSocketClient the transport client to use
|
||||
*/
|
||||
static Builder<?> builder(WebSocketClient webSocketClient) {
|
||||
return new DefaultWebSocketGraphQlClient.Builder(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);
|
||||
|
||||
/**
|
||||
* Build the {@code WebSocketGraphQlClient}.
|
||||
*/
|
||||
@Override
|
||||
WebSocketGraphQlClient build();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.time.Duration;
|
||||
import java.util.Collections;
|
||||
|
||||
import graphql.ExecutionResult;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.graphql.support.MapExecutionResult;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link GraphQlClient.Builder}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class WebSocketGraphQlClientBuilderTests {
|
||||
|
||||
private TestWebSocketClient webSocketClient;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockGraphQlWebSocketServer mockServer = new MockGraphQlWebSocketServer();
|
||||
this.webSocketClient = new TestWebSocketClient(mockServer);
|
||||
|
||||
ExecutionResult result = MapExecutionResult.forDataOnly(Collections.singletonMap("key1", "value1"));
|
||||
mockServer.expectOperation("{Query1}").andRespond(result);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void mutate() {
|
||||
|
||||
// Original
|
||||
|
||||
URI url = URI.create("/graphql");
|
||||
|
||||
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(this.webSocketClient)
|
||||
.url(url)
|
||||
.header("header1", "value1")
|
||||
.header("header2", "value2")
|
||||
.build();
|
||||
|
||||
graphQlClient.document("{Query1}").execute().block(Duration.ofSeconds(5));
|
||||
|
||||
assertThat(this.webSocketClient.getConnectionCount()).isEqualTo(1);
|
||||
assertThat(this.webSocketClient.getConnection(0).getUrl()).isEqualTo(url);
|
||||
assertThat(this.webSocketClient.getConnection(0).getHeaders()).hasSize(2)
|
||||
.containsEntry("header1", Collections.singletonList("value1"))
|
||||
.containsEntry("header2", Collections.singletonList("value2"));
|
||||
|
||||
// Mutate
|
||||
|
||||
URI anotherUrl = URI.create("/another-graphql");
|
||||
|
||||
WebSocketGraphQlClient anotherClient = graphQlClient.mutate()
|
||||
.url(anotherUrl)
|
||||
.headers(headers -> {
|
||||
headers.set("header1", "anotherValue1");
|
||||
headers.set("header2", "anotherValue2");
|
||||
})
|
||||
.build();
|
||||
|
||||
anotherClient.document("{Query1}").execute().block(Duration.ofSeconds(5));
|
||||
|
||||
assertThat(this.webSocketClient.getConnectionCount()).isEqualTo(2);
|
||||
assertThat(this.webSocketClient.getConnection(1).getUrl()).isEqualTo(anotherUrl);
|
||||
assertThat(this.webSocketClient.getConnection(1).getHeaders()).hasSize(2)
|
||||
.containsEntry("header1", Collections.singletonList("anotherValue1"))
|
||||
.containsEntry("header2", Collections.singletonList("anotherValue2"));
|
||||
|
||||
// Original not affected (stop + start original client, to connect again)
|
||||
|
||||
graphQlClient.stop().block(Duration.ofSeconds(5));
|
||||
graphQlClient.start().block(Duration.ofSeconds(5));
|
||||
graphQlClient.document("{Query1}").execute().block();
|
||||
|
||||
assertThat(this.webSocketClient.getConnection(0).getUrl()).isEqualTo(url);
|
||||
assertThat(this.webSocketClient.getConnection(0).getHeaders()).hasSize(2)
|
||||
.containsEntry("header1", Collections.singletonList("value1"))
|
||||
.containsEntry("header2", Collections.singletonList("value2"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.BookCriteria;
|
||||
import org.springframework.graphql.BookSource;
|
||||
import org.springframework.graphql.GraphQlSetup;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
import org.springframework.graphql.web.webflux.GraphQlWebSocketHandler;
|
||||
import org.springframework.http.codec.ClientCodecConfigurer;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.reactive.socket.WebSocketHandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test making requests through {@link WebSocketGraphQlClient} to
|
||||
* {@link GraphQlWebSocketHandler} via a mock WebSocket connection.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class WebSocketGraphQlClientTests {
|
||||
|
||||
private final TestWebSocketClient webSocketClient = initWebSocketClient();
|
||||
|
||||
|
||||
@Test
|
||||
void query() {
|
||||
String document = "{ " +
|
||||
" booksByCriteria(criteria: {author:\"Orwell\"}) { " +
|
||||
" id" +
|
||||
" name" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
List<Book> books = WebSocketGraphQlClient.create(URI.create("/"), this.webSocketClient)
|
||||
.document(document)
|
||||
.execute()
|
||||
.map(response -> response.toEntityList("booksByCriteria", Book.class))
|
||||
.block(Duration.ofSeconds(5));
|
||||
|
||||
assertThat(books).hasSize(2);
|
||||
assertThat(books.get(0).getName()).isEqualTo("Nineteen Eighty-Four");
|
||||
assertThat(books.get(1).getName()).isEqualTo("Animal Farm");
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscription() {
|
||||
String document = "subscription { " +
|
||||
" bookSearch(author:\"Orwell\") { " +
|
||||
" id" +
|
||||
" name" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
Flux<Book> bookFlux = WebSocketGraphQlClient.create(URI.create("/"), this.webSocketClient)
|
||||
.document(document)
|
||||
.executeSubscription()
|
||||
.map(response -> response.toEntity("bookSearch", Book.class));
|
||||
|
||||
StepVerifier.create(bookFlux)
|
||||
.consumeNextWith(book -> {
|
||||
assertThat(book.getId()).isEqualTo(1);
|
||||
assertThat(book.getName()).isEqualTo("Nineteen Eighty-Four");
|
||||
})
|
||||
.consumeNextWith(book -> {
|
||||
assertThat(book.getId()).isEqualTo(5);
|
||||
assertThat(book.getName()).isEqualTo("Animal Farm");
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
|
||||
private TestWebSocketClient initWebSocketClient() {
|
||||
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.register(BookController.class);
|
||||
context.refresh();
|
||||
|
||||
WebGraphQlHandler webGraphQlHandler = GraphQlSetup.schemaResource(BookSource.schema)
|
||||
.runtimeWiringForAnnotatedControllers(context)
|
||||
.toWebGraphQlHandler();
|
||||
|
||||
WebSocketHandler webSocketServerHandler = new GraphQlWebSocketHandler(
|
||||
webGraphQlHandler, ClientCodecConfigurer.create(), Duration.ofSeconds(5));
|
||||
|
||||
return new TestWebSocketClient(webSocketServerHandler);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Controller
|
||||
private static class BookController {
|
||||
|
||||
@QueryMapping
|
||||
public List<Book> booksByCriteria(@Argument BookCriteria criteria) {
|
||||
return BookSource.findBooksByAuthor(criteria.getAuthor());
|
||||
}
|
||||
|
||||
@SubscriptionMapping
|
||||
public Flux<Book> bookSearch(@Argument String author) {
|
||||
return Flux.fromIterable(BookSource.findBooksByAuthor(author)).delayElements(Duration.ofMillis(50));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user