Introduce blocking execution in GraphQlClient
See gh-771
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -66,6 +67,9 @@ public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClie
|
||||
@Nullable
|
||||
private Decoder<?> jsonDecoder;
|
||||
|
||||
@Nullable
|
||||
private Duration blockingTimeout;
|
||||
|
||||
|
||||
/**
|
||||
* Default constructor for use from subclasses.
|
||||
@@ -82,7 +86,6 @@ public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClie
|
||||
ResourceDocumentSource.FILE_EXTENSIONS));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public B interceptor(GraphQlClientInterceptor... interceptors) {
|
||||
this.interceptors.addAll(Arrays.asList(interceptors));
|
||||
@@ -101,6 +104,12 @@ public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClie
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public B blockingTimeout(@Nullable Duration blockingTimeout) {
|
||||
this.blockingTimeout = blockingTimeout;
|
||||
return self();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends B> T self() {
|
||||
return (T) this;
|
||||
@@ -169,8 +178,8 @@ public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClie
|
||||
this.jsonDecoder = (this.jsonDecoder == null ? DefaultJackson2Codecs.decoder() : this.jsonDecoder);
|
||||
}
|
||||
|
||||
return new DefaultGraphQlClient(
|
||||
this.documentSource, createExecuteChain(transport), createExecuteSubscriptionChain(transport));
|
||||
return new DefaultGraphQlClient(this.documentSource,
|
||||
createExecuteChain(transport), createSubscriptionChain(transport), this.blockingTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,23 +195,25 @@ public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClie
|
||||
|
||||
private Chain createExecuteChain(GraphQlTransport transport) {
|
||||
|
||||
Chain chain = request -> transport.execute(request).map(response ->
|
||||
new DefaultClientGraphQlResponse(request, response, getEncoder(), getDecoder()));
|
||||
|
||||
return this.interceptors.stream()
|
||||
.reduce(GraphQlClientInterceptor::andThen)
|
||||
.map(interceptor -> (Chain) (request) -> interceptor.intercept(request, chain))
|
||||
.orElse(chain);
|
||||
}
|
||||
|
||||
private SubscriptionChain createExecuteSubscriptionChain(GraphQlTransport transport) {
|
||||
|
||||
SubscriptionChain chain = request -> transport.executeSubscription(request)
|
||||
Chain chain = request -> transport
|
||||
.execute(request)
|
||||
.map(response -> new DefaultClientGraphQlResponse(request, response, getEncoder(), getDecoder()));
|
||||
|
||||
return this.interceptors.stream()
|
||||
.reduce(GraphQlClientInterceptor::andThen)
|
||||
.map(interceptor -> (SubscriptionChain) (request) -> interceptor.interceptSubscription(request, chain))
|
||||
.map(i -> (Chain) (request) -> i.intercept(request, chain))
|
||||
.orElse(chain);
|
||||
}
|
||||
|
||||
private SubscriptionChain createSubscriptionChain(GraphQlTransport transport) {
|
||||
|
||||
SubscriptionChain chain = request -> transport
|
||||
.executeSubscription(request)
|
||||
.map(response -> new DefaultClientGraphQlResponse(request, response, getEncoder(), getDecoder()));
|
||||
|
||||
return this.interceptors.stream()
|
||||
.reduce(GraphQlClientInterceptor::andThen)
|
||||
.map(i -> (SubscriptionChain) (request) -> i.interceptSubscription(request, chain))
|
||||
.orElse(chain);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.graphql.GraphQlResponse;
|
||||
import org.springframework.graphql.client.SyncGraphQlClientInterceptor.Chain;
|
||||
import org.springframework.graphql.support.CachingDocumentSource;
|
||||
import org.springframework.graphql.support.DocumentSource;
|
||||
import org.springframework.graphql.support.ResourceDocumentSource;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
|
||||
/**
|
||||
* Abstract, base class for transport specific {@link GraphQlClient.SyncBuilder}
|
||||
* implementations.
|
||||
*
|
||||
* <p>Subclasses must implement {@link #build()} and call
|
||||
* {@link #buildGraphQlClient(SyncGraphQlTransport)} 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.3
|
||||
* @see AbstractDelegatingGraphQlClient
|
||||
*/
|
||||
public abstract class AbstractGraphQlClientSyncBuilder<B extends AbstractGraphQlClientSyncBuilder<B>>
|
||||
implements GraphQlClient.SyncBuilder<B> {
|
||||
|
||||
protected static final boolean jackson2Present = ClassUtils.isPresent(
|
||||
"com.fasterxml.jackson.databind.ObjectMapper", AbstractGraphQlClientSyncBuilder.class.getClassLoader());
|
||||
|
||||
|
||||
private final List<SyncGraphQlClientInterceptor> interceptors = new ArrayList<>();
|
||||
|
||||
private DocumentSource documentSource;
|
||||
|
||||
@Nullable
|
||||
private HttpMessageConverter<Object> jsonConverter;
|
||||
|
||||
private Scheduler scheduler = Schedulers.boundedElastic();
|
||||
|
||||
@Nullable
|
||||
private Duration blockingTimeout;
|
||||
|
||||
/**
|
||||
* Default constructor for use from subclasses.
|
||||
* <p>Subclasses must set the transport to use before {@link #build()} or
|
||||
* during, by overriding {@link #build()}.
|
||||
*/
|
||||
protected AbstractGraphQlClientSyncBuilder() {
|
||||
this.documentSource = initDocumentSource();
|
||||
}
|
||||
|
||||
private static DocumentSource initDocumentSource() {
|
||||
return new CachingDocumentSource(new ResourceDocumentSource(
|
||||
Collections.singletonList(new ClassPathResource("graphql-documents/")),
|
||||
ResourceDocumentSource.FILE_EXTENSIONS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public B interceptor(SyncGraphQlClientInterceptor... interceptors) {
|
||||
Collections.addAll(this.interceptors, interceptors);
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public B interceptors(Consumer<List<SyncGraphQlClientInterceptor>> interceptorsConsumer) {
|
||||
interceptorsConsumer.accept(this.interceptors);
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public B documentSource(DocumentSource contentLoader) {
|
||||
this.documentSource = contentLoader;
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public B scheduler(Scheduler scheduler) {
|
||||
Assert.notNull(scheduler, "Scheduler is required");
|
||||
this.scheduler = scheduler;
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public B blockingTimeout(@Nullable Duration blockingTimeout) {
|
||||
this.blockingTimeout = blockingTimeout;
|
||||
return self();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends B> T self() {
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
|
||||
// Protected methods for use from build() in subclasses
|
||||
|
||||
|
||||
/**
|
||||
* Transport-specific subclasses can provide their JSON {@code Encoder} and
|
||||
* {@code Decoder} for use at the client level, for mapping response data
|
||||
* to some target entity type.
|
||||
*/
|
||||
protected void setJsonConverter(HttpMessageConverter<Object> converter) {
|
||||
this.jsonConverter = converter;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build the default transport-agnostic client that subclasses can then wrap
|
||||
* with {@link AbstractDelegatingGraphQlClient}.
|
||||
*/
|
||||
protected GraphQlClient buildGraphQlClient(SyncGraphQlTransport transport) {
|
||||
|
||||
if (jackson2Present) {
|
||||
this.jsonConverter = (this.jsonConverter == null ?
|
||||
DefaultJacksonConverter.initialize() : this.jsonConverter);
|
||||
}
|
||||
|
||||
return new DefaultGraphQlClient(
|
||||
this.documentSource, createExecuteChain(transport), this.scheduler, this.blockingTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code Consumer} to initialize new builders from "this" builder.
|
||||
*/
|
||||
protected Consumer<AbstractGraphQlClientSyncBuilder<?>> getBuilderInitializer() {
|
||||
return builder -> {
|
||||
builder.interceptors(interceptorList -> interceptorList.addAll(interceptors));
|
||||
builder.documentSource(documentSource);
|
||||
builder.setJsonConverter(getJsonConverter());
|
||||
};
|
||||
}
|
||||
|
||||
private Chain createExecuteChain(SyncGraphQlTransport transport) {
|
||||
|
||||
Encoder<?> encoder = HttpMessageConverterDelegate.asEncoder(getJsonConverter());
|
||||
Decoder<?> decoder = HttpMessageConverterDelegate.asDecoder(getJsonConverter());
|
||||
|
||||
Chain chain = request -> {
|
||||
GraphQlResponse response = transport.execute(request);
|
||||
return new DefaultClientGraphQlResponse(request, response, encoder, decoder);
|
||||
};
|
||||
|
||||
return this.interceptors.stream()
|
||||
.reduce(SyncGraphQlClientInterceptor::andThen)
|
||||
.map(i -> (Chain) (request) -> i.intercept(request, chain))
|
||||
.orElse(chain);
|
||||
}
|
||||
|
||||
private HttpMessageConverter<Object> getJsonConverter() {
|
||||
Assert.notNull(this.jsonConverter, "jsonConverter has not been set");
|
||||
return this.jsonConverter;
|
||||
}
|
||||
|
||||
|
||||
private static class DefaultJacksonConverter {
|
||||
|
||||
static HttpMessageConverter<Object> initialize() {
|
||||
return new MappingJackson2HttpMessageConverter();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -24,6 +25,7 @@ import java.util.function.Consumer;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.support.DocumentSource;
|
||||
@@ -40,22 +42,60 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
|
||||
private final DocumentSource documentSource;
|
||||
|
||||
private final GraphQlClientInterceptor.Chain executeChain;
|
||||
private final SyncGraphQlClientInterceptor.Chain blockingChain;
|
||||
|
||||
private final GraphQlClientInterceptor.Chain nonBlockingChain;
|
||||
|
||||
private final GraphQlClientInterceptor.SubscriptionChain executeSubscriptionChain;
|
||||
|
||||
@Nullable
|
||||
private final Duration blockingTimeout;
|
||||
|
||||
|
||||
DefaultGraphQlClient(
|
||||
DocumentSource documentSource, GraphQlClientInterceptor.Chain executeChain,
|
||||
GraphQlClientInterceptor.SubscriptionChain executeSubscriptionChain) {
|
||||
DocumentSource documentSource, SyncGraphQlClientInterceptor.Chain blockingChain,
|
||||
Scheduler scheduler, @Nullable Duration blockingTimeout) {
|
||||
|
||||
Assert.notNull(documentSource, "DocumentSource is required");
|
||||
Assert.notNull(executeChain, "GraphQlClientInterceptor.Chain is required");
|
||||
Assert.notNull(executeSubscriptionChain, "GraphQlClientInterceptor.SubscriptionChain is required");
|
||||
Assert.notNull(blockingChain, "Execution chain is required");
|
||||
Assert.notNull(scheduler, "Scheduler is required");
|
||||
|
||||
this.documentSource = documentSource;
|
||||
this.executeChain = executeChain;
|
||||
this.executeSubscriptionChain = executeSubscriptionChain;
|
||||
this.blockingChain = blockingChain;
|
||||
this.nonBlockingChain = adaptToNonBlockingChain(blockingChain, scheduler);
|
||||
this.executeSubscriptionChain = request -> Flux.error(new IllegalStateException("Subscriptions on supported"));
|
||||
this.blockingTimeout = blockingTimeout;
|
||||
}
|
||||
|
||||
DefaultGraphQlClient(
|
||||
DocumentSource documentSource,
|
||||
GraphQlClientInterceptor.Chain nonBlockingChain,
|
||||
GraphQlClientInterceptor.SubscriptionChain subscriptionChain,
|
||||
@Nullable Duration blockingTimeout) {
|
||||
|
||||
Assert.notNull(documentSource, "DocumentSource is required");
|
||||
Assert.notNull(nonBlockingChain, "Execution chain is required");
|
||||
Assert.notNull(subscriptionChain, "Subscription execution chain is required");
|
||||
|
||||
this.documentSource = documentSource;
|
||||
this.blockingChain = adaptToBlockingChain(nonBlockingChain, blockingTimeout);
|
||||
this.nonBlockingChain = nonBlockingChain;
|
||||
this.executeSubscriptionChain = subscriptionChain;
|
||||
this.blockingTimeout = blockingTimeout;
|
||||
}
|
||||
|
||||
private static GraphQlClientInterceptor.Chain adaptToNonBlockingChain(
|
||||
SyncGraphQlClientInterceptor.Chain blockingChain, Scheduler scheduler) {
|
||||
|
||||
return request -> Mono.fromCallable(() -> blockingChain.next(request)).subscribeOn(scheduler);
|
||||
}
|
||||
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
private static SyncGraphQlClientInterceptor.Chain adaptToBlockingChain(
|
||||
GraphQlClientInterceptor.Chain executeChain, @Nullable Duration blockingTimeout) {
|
||||
|
||||
return (request -> blockingTimeout != null ?
|
||||
executeChain.next(request).block(blockingTimeout) : executeChain.next(request).block());
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +160,7 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestSpec extension(String name, Object value) {
|
||||
public RequestSpec extension(String name, @Nullable Object value) {
|
||||
this.extensions.put(name, value);
|
||||
return this;
|
||||
}
|
||||
@@ -143,6 +183,12 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RetrieveSyncSpec retrieveSync(String path) {
|
||||
ClientGraphQlResponse response = executeSync();
|
||||
return new DefaultRetrieveSyncSpec(response, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RetrieveSpec retrieve(String path) {
|
||||
return new DefaultRetrieveSpec(execute(), path);
|
||||
@@ -153,9 +199,17 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
return new DefaultRetrieveSubscriptionSpec(executeSubscription(), path);
|
||||
}
|
||||
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
@Override
|
||||
public ClientGraphQlResponse executeSync() {
|
||||
Mono<ClientGraphQlRequest> mono = initRequest();
|
||||
ClientGraphQlRequest request = (blockingTimeout != null ? mono.block(blockingTimeout) : mono.block());
|
||||
return blockingChain.next(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ClientGraphQlResponse> execute() {
|
||||
return initRequest().flatMap(request -> executeChain.next(request)
|
||||
return initRequest().flatMap(request -> nonBlockingChain.next(request)
|
||||
.onErrorResume(
|
||||
ex -> !(ex instanceof GraphQlClientException),
|
||||
ex -> Mono.error(new GraphQlTransportException(ex, request))));
|
||||
@@ -170,8 +224,8 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
}
|
||||
|
||||
private Mono<ClientGraphQlRequest> initRequest() {
|
||||
return this.documentMono.map(document ->
|
||||
new DefaultClientGraphQlRequest(document, this.operationName, this.variables, this.extensions, this.attributes));
|
||||
return this.documentMono.map(document -> new DefaultClientGraphQlRequest(
|
||||
document, this.operationName, this.variables, this.extensions, this.attributes));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -204,6 +258,42 @@ final class DefaultGraphQlClient implements GraphQlClient {
|
||||
}
|
||||
|
||||
|
||||
private static class DefaultRetrieveSyncSpec extends RetrieveSpecSupport implements RetrieveSyncSpec {
|
||||
|
||||
private final ClientGraphQlResponse response;
|
||||
|
||||
DefaultRetrieveSyncSpec(ClientGraphQlResponse response, String path) {
|
||||
super(path);
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> D toEntity(Class<D> entityType) {
|
||||
ClientResponseField field = getValidField(this.response);
|
||||
return (field != null ? field.toEntity(entityType) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> D toEntity(ParameterizedTypeReference<D> entityType) {
|
||||
ClientResponseField field = getValidField(this.response);
|
||||
return (field != null ? field.toEntity(entityType) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(Class<D> elementType) {
|
||||
ClientResponseField field = getValidField(this.response);
|
||||
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D> List<D> toEntityList(ParameterizedTypeReference<D> elementType) {
|
||||
ClientResponseField field = getValidField(this.response);
|
||||
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class DefaultRetrieveSpec extends RetrieveSpecSupport implements RetrieveSpec {
|
||||
|
||||
private final Mono<ClientGraphQlResponse> responseMono;
|
||||
|
||||
@@ -21,10 +21,7 @@ import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.codec.ClientCodecConfigurer;
|
||||
import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
@@ -33,124 +30,107 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
|
||||
/**
|
||||
* Default {@link RestClientGraphQlClient.Builder} implementation, a simple wrapper
|
||||
* Default {@link HttpSyncGraphQlClient.Builder} implementation, a simple wrapper
|
||||
* around a {@link RestClient.Builder}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.3
|
||||
*/
|
||||
final class DefaultRestClientGraphQlClientBuilder
|
||||
extends AbstractGraphQlClientBuilder<DefaultRestClientGraphQlClientBuilder>
|
||||
implements RestClientGraphQlClient.Builder<DefaultRestClientGraphQlClientBuilder> {
|
||||
final class DefaultSyncHttpGraphQlClientBuilder
|
||||
extends AbstractGraphQlClientSyncBuilder<DefaultSyncHttpGraphQlClientBuilder>
|
||||
implements HttpSyncGraphQlClient.Builder<DefaultSyncHttpGraphQlClientBuilder> {
|
||||
|
||||
private final RestClient.Builder restClientBuilder;
|
||||
|
||||
@Nullable
|
||||
private CodecConfigurer codecConfigurer;
|
||||
|
||||
/**
|
||||
* Constructor to start without a RestClient instance.
|
||||
*/
|
||||
DefaultRestClientGraphQlClientBuilder() {
|
||||
DefaultSyncHttpGraphQlClientBuilder() {
|
||||
this(RestClient.builder());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to start with a pre-configured {@code RestClient}.
|
||||
*/
|
||||
DefaultRestClientGraphQlClientBuilder(RestClient client) {
|
||||
DefaultSyncHttpGraphQlClientBuilder(RestClient client) {
|
||||
this(client.mutate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to start with a pre-configured {@code RestClient}.
|
||||
*/
|
||||
DefaultRestClientGraphQlClientBuilder(RestClient.Builder clientBuilder) {
|
||||
DefaultSyncHttpGraphQlClientBuilder(RestClient.Builder clientBuilder) {
|
||||
this.restClientBuilder = clientBuilder;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder url(String url) {
|
||||
public DefaultSyncHttpGraphQlClientBuilder url(String url) {
|
||||
this.restClientBuilder.baseUrl(url);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder url(URI url) {
|
||||
public DefaultSyncHttpGraphQlClientBuilder url(URI url) {
|
||||
UriBuilderFactory factory = new DefaultUriBuilderFactory(UriComponentsBuilder.fromUri(url));
|
||||
this.restClientBuilder.uriBuilderFactory(factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder header(String name, String... values) {
|
||||
public DefaultSyncHttpGraphQlClientBuilder header(String name, String... values) {
|
||||
this.restClientBuilder.defaultHeader(name, values);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder headers(Consumer<HttpHeaders> headersConsumer) {
|
||||
public DefaultSyncHttpGraphQlClientBuilder headers(Consumer<HttpHeaders> headersConsumer) {
|
||||
this.restClientBuilder.defaultHeaders(headersConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder codecConfigurer(Consumer<CodecConfigurer> codecConsumer) {
|
||||
if (this.codecConfigurer == null) {
|
||||
this.codecConfigurer = ClientCodecConfigurer.create();
|
||||
}
|
||||
codecConsumer.accept(this.codecConfigurer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder messageConverters(Consumer<List<HttpMessageConverter<?>>> configurer) {
|
||||
public DefaultSyncHttpGraphQlClientBuilder messageConverters(Consumer<List<HttpMessageConverter<?>>> configurer) {
|
||||
this.restClientBuilder.messageConverters(configurer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRestClientGraphQlClientBuilder restClient(Consumer<RestClient.Builder> configurer) {
|
||||
public DefaultSyncHttpGraphQlClientBuilder restClient(Consumer<RestClient.Builder> configurer) {
|
||||
configurer.accept(this.restClientBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestClientGraphQlClient build() {
|
||||
public HttpSyncGraphQlClient build() {
|
||||
|
||||
// Pass the codecs to the parent for response decoding
|
||||
if (this.codecConfigurer != null) {
|
||||
setJsonEncoder(CodecDelegate.findJsonEncoder(this.codecConfigurer));
|
||||
setJsonDecoder(CodecDelegate.findJsonDecoder(this.codecConfigurer));
|
||||
}
|
||||
else {
|
||||
this.restClientBuilder.messageConverters(converters -> {
|
||||
setJsonEncoder(HttpMessageConverterDelegate.getJsonEncoder(converters));
|
||||
setJsonDecoder(HttpMessageConverterDelegate.getJsonDecoder(converters));
|
||||
});
|
||||
}
|
||||
this.restClientBuilder.messageConverters(converters -> {
|
||||
HttpMessageConverter<Object> converter = HttpMessageConverterDelegate.findJsonConverter(converters);
|
||||
setJsonConverter(converter);
|
||||
});
|
||||
|
||||
RestClient restClient = this.restClientBuilder.build();
|
||||
HttpSyncGraphQlTransport syncTransport = new HttpSyncGraphQlTransport(restClient);
|
||||
|
||||
GraphQlClient graphQlClient = super.buildGraphQlClient(new RestClientGraphQlTransport(restClient, null));
|
||||
return new DefaultRestClientGraphQlClient(graphQlClient, restClient, getBuilderInitializer());
|
||||
GraphQlClient graphQlClient = super.buildGraphQlClient(syncTransport);
|
||||
return new DefaultHttpSyncGraphQlClient(graphQlClient, restClient, getBuilderInitializer());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default {@link HttpGraphQlClient} implementation.
|
||||
*/
|
||||
private static class DefaultRestClientGraphQlClient
|
||||
extends AbstractDelegatingGraphQlClient implements RestClientGraphQlClient {
|
||||
private static class DefaultHttpSyncGraphQlClient
|
||||
extends AbstractDelegatingGraphQlClient implements HttpSyncGraphQlClient {
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
private final Consumer<AbstractGraphQlClientBuilder<?>> builderInitializer;
|
||||
private final Consumer<AbstractGraphQlClientSyncBuilder<?>> builderInitializer;
|
||||
|
||||
DefaultRestClientGraphQlClient(
|
||||
DefaultHttpSyncGraphQlClient(
|
||||
GraphQlClient delegate, RestClient restClient,
|
||||
Consumer<AbstractGraphQlClientBuilder<?>> builderInitializer) {
|
||||
Consumer<AbstractGraphQlClientSyncBuilder<?>> builderInitializer) {
|
||||
|
||||
super(delegate);
|
||||
|
||||
@@ -161,8 +141,8 @@ final class DefaultRestClientGraphQlClientBuilder
|
||||
this.builderInitializer = builderInitializer;
|
||||
}
|
||||
|
||||
public DefaultRestClientGraphQlClientBuilder mutate() {
|
||||
DefaultRestClientGraphQlClientBuilder builder = new DefaultRestClientGraphQlClientBuilder(this.restClient);
|
||||
public DefaultSyncHttpGraphQlClientBuilder mutate() {
|
||||
DefaultSyncHttpGraphQlClientBuilder builder = new DefaultSyncHttpGraphQlClientBuilder(this.restClient);
|
||||
this.builderInitializer.accept(builder);
|
||||
return builder;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -15,12 +15,15 @@
|
||||
*/
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.GraphQlResponse;
|
||||
@@ -69,7 +72,7 @@ public interface GraphQlClient {
|
||||
* Return a builder initialized from the configuration of "this" client
|
||||
* to use to build a new, independently configured client instance.
|
||||
*/
|
||||
GraphQlClient.Builder<?> mutate();
|
||||
BaseBuilder<?> mutate();
|
||||
|
||||
|
||||
/**
|
||||
@@ -86,9 +89,77 @@ public interface GraphQlClient {
|
||||
|
||||
|
||||
/**
|
||||
* Defines a builder for creating {@link GraphQlClient} instances.
|
||||
* Base builder to create a {@link GraphQlClient}.
|
||||
* @since 1.3
|
||||
*/
|
||||
interface Builder<B extends Builder<B>> {
|
||||
interface BaseBuilder<B extends BaseBuilder<B>> {
|
||||
|
||||
/**
|
||||
* Configure a {@link DocumentSource} for use with
|
||||
* {@link #documentName(String)} for resolving a document by name.
|
||||
* <p>By default, this is set to {@link ResourceDocumentSource} with
|
||||
* classpath location {@code "graphql-documents/"} and
|
||||
* {@link ResourceDocumentSource#FILE_EXTENSIONS} as extensions.
|
||||
*/
|
||||
B documentSource(DocumentSource contentLoader);
|
||||
|
||||
/**
|
||||
* Configure a timeout to use for blocking execution.
|
||||
* <p>By default this is not set, in which case the behavior depends on
|
||||
* connection and request timeout settings of the underlying transport.
|
||||
* We recommend configuring timeout values directly on the underlying
|
||||
* transport, which provides more control over such settings.
|
||||
* @param blockingTimeout the timeout to use
|
||||
*/
|
||||
B blockingTimeout(@Nullable Duration blockingTimeout);
|
||||
|
||||
/**
|
||||
* Build the {@code GraphQlClient} instance.
|
||||
*/
|
||||
GraphQlClient build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builder to create a {@link GraphQlClient} instance with a
|
||||
* synchronous transport and interceptors.
|
||||
* @since 1.3
|
||||
* @see SyncGraphQlTransport
|
||||
*/
|
||||
interface SyncBuilder<B extends SyncBuilder<B>> extends BaseBuilder<B> {
|
||||
|
||||
/**
|
||||
* Configure interceptors to be invoked before delegating to the
|
||||
* {@link SyncGraphQlTransport} to perform the request.
|
||||
* @param interceptors the interceptors to add
|
||||
* @return this builder
|
||||
*/
|
||||
B interceptor(SyncGraphQlClientInterceptor... interceptors);
|
||||
|
||||
/**
|
||||
* Customize the list of interceptors. The provided list is "live", so
|
||||
* the consumer can inspect and insert interceptors accordingly.
|
||||
* @param interceptorsConsumer consumer to customize the interceptors with
|
||||
* @return this builder
|
||||
*/
|
||||
B interceptors(Consumer<List<SyncGraphQlClientInterceptor>> interceptorsConsumer);
|
||||
|
||||
/**
|
||||
* The scheduler to use for non-blocking execution with
|
||||
* {@link RequestSpec#execute()} and {@link RequestSpec#retrieve(String)}.
|
||||
* <p>By default this is set to {@link Schedulers#boundedElastic()}.
|
||||
* @param scheduler the scheduler
|
||||
*/
|
||||
B scheduler(Scheduler scheduler);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builder to create {@link GraphQlClient} instances with a non-blocking
|
||||
* {@link GraphQlTransport} and interceptors.
|
||||
*/
|
||||
interface Builder<B extends Builder<B>> extends BaseBuilder<B> {
|
||||
|
||||
/**
|
||||
* Configure interceptors to be invoked before delegating to the
|
||||
@@ -106,20 +177,6 @@ public interface GraphQlClient {
|
||||
*/
|
||||
B interceptors(Consumer<List<GraphQlClientInterceptor>> interceptorsConsumer);
|
||||
|
||||
/**
|
||||
* Configure a {@link DocumentSource} for use with
|
||||
* {@link #documentName(String)} for resolving a document by name.
|
||||
* <p>By default, this is set to {@link ResourceDocumentSource} with
|
||||
* classpath location {@code "graphql-documents/"} and
|
||||
* {@link ResourceDocumentSource#FILE_EXTENSIONS} as extensions.
|
||||
*/
|
||||
B documentSource(DocumentSource contentLoader);
|
||||
|
||||
/**
|
||||
* Build the {@code GraphQlClient} instance.
|
||||
*/
|
||||
GraphQlClient build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +241,19 @@ public interface GraphQlClient {
|
||||
*/
|
||||
RequestSpec attributes(Consumer<Map<String, Object>> attributesConsumer);
|
||||
|
||||
/**
|
||||
* Shortcut for {@link #execute()} with a field path to decode from.
|
||||
* <p>If you want to decode the full data instead, use {@link #execute()}:
|
||||
* <pre>
|
||||
* client.document("..").execute().map(response -> response.toEntity(..))
|
||||
* </pre>
|
||||
* @return a spec with decoding options
|
||||
* @throws FieldAccessException if the field has any field errors,
|
||||
* including errors at, above or below the field path.
|
||||
* @since 1.3
|
||||
*/
|
||||
RetrieveSyncSpec retrieveSync(String path);
|
||||
|
||||
/**
|
||||
* Shortcut for {@link #execute()} with a field path to decode from.
|
||||
* <p>If you want to decode the full data instead, use {@link #execute()}:
|
||||
@@ -207,12 +277,22 @@ public interface GraphQlClient {
|
||||
*/
|
||||
RetrieveSubscriptionSpec retrieveSubscription(String path);
|
||||
|
||||
/**
|
||||
* Execute request with a single response, e.g. "query" or "mutation", and
|
||||
* return a response for further options.
|
||||
* @return a {@code ClientGraphQlResponse} for further decoding of the response.
|
||||
* @throws GraphQlTransportException in case of errors due to transport or
|
||||
* other issues related to encoding and decoding the request and response.
|
||||
* @since 1.3
|
||||
*/
|
||||
ClientGraphQlResponse executeSync();
|
||||
|
||||
/**
|
||||
* Execute request with a single response, e.g. "query" or "mutation", and
|
||||
* return a response for further options.
|
||||
* @return a {@code Mono} with a {@code ClientGraphQlResponse} for further
|
||||
* decoding of the response. The {@code Mono} may end wth an error due
|
||||
* to transport level issues.
|
||||
* decoding of the response. The {@code Mono} may end with a
|
||||
* .
|
||||
*/
|
||||
Mono<ClientGraphQlResponse> execute();
|
||||
|
||||
@@ -235,6 +315,44 @@ public interface GraphQlClient {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Declares options to decode a field for a single response operation.
|
||||
*/
|
||||
interface RetrieveSyncSpec {
|
||||
|
||||
/**
|
||||
* Decode the field to an entity of the given type.
|
||||
* @param entityType the type to convert to
|
||||
* @return {@code Mono} with the decoded entity; completes with
|
||||
* {@link FieldAccessException} in case of {@link ResponseField field
|
||||
* errors} or an {@link GraphQlResponse#isValid() invalid} response;
|
||||
* completes empty if the field is {@code null} but has no errors.
|
||||
* @see ResponseField#getErrors()
|
||||
*/
|
||||
@Nullable
|
||||
<D> D toEntity(Class<D> entityType);
|
||||
|
||||
/**
|
||||
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
|
||||
*/
|
||||
@Nullable
|
||||
<D> D toEntity(ParameterizedTypeReference<D> entityType);
|
||||
|
||||
/**
|
||||
* Variant of {@link #toEntity(Class)} to decode to a List of entities.
|
||||
* @param elementType the type of elements in the list
|
||||
*/
|
||||
<D> List<D> toEntityList(Class<D> elementType);
|
||||
|
||||
/**
|
||||
* Variant of {@link #toEntity(Class)} to decode to a List of entities.
|
||||
* @param elementType the type of elements in the list
|
||||
*/
|
||||
<D> List<D> toEntityList(ParameterizedTypeReference<D> elementType);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Declares options to decode a field for a single response operation.
|
||||
*/
|
||||
|
||||
@@ -48,26 +48,18 @@ import org.springframework.util.MimeType;
|
||||
|
||||
|
||||
/**
|
||||
* Helper class to adapt JSON {@link HttpMessageConverter} to
|
||||
* {@link Encoder} and {@link Decoder}.
|
||||
* {@link DefaultClientGraphQlResponse} uses {@link Encoder} and {@link Decoder}
|
||||
* to encode the response map to JSON and then encode it into higher level
|
||||
* objects. This delegate helps with finding an {@link HttpMessageConverter}
|
||||
* for JSON and adapt it to {@link Encoder} and {@link Decoder}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.3
|
||||
*/
|
||||
final class HttpMessageConverterDelegate {
|
||||
|
||||
static Encoder<?> getJsonEncoder(List<HttpMessageConverter<?>> converters) {
|
||||
HttpMessageConverter<Object> converter = findJsonConverter(converters);
|
||||
return new HttpMessageConverterEncoder(converter);
|
||||
}
|
||||
|
||||
static Decoder<?> getJsonDecoder(List<HttpMessageConverter<?>> converters) {
|
||||
HttpMessageConverter<Object> converter = findJsonConverter(converters);
|
||||
return new HttpMessageConverterDecoder(converter);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static HttpMessageConverter<Object> findJsonConverter(List<HttpMessageConverter<?>> converters) {
|
||||
static HttpMessageConverter<Object> findJsonConverter(List<HttpMessageConverter<?>> converters) {
|
||||
return (HttpMessageConverter<Object>) converters.stream()
|
||||
.filter(converter -> converter.canRead(Map.class, MediaType.APPLICATION_JSON))
|
||||
.findFirst()
|
||||
@@ -82,6 +74,14 @@ final class HttpMessageConverterDelegate {
|
||||
return (mimeType != null ? new MediaType(mimeType) : null);
|
||||
}
|
||||
|
||||
static HttpMessageConverterEncoder asEncoder(HttpMessageConverter<Object> converter) {
|
||||
return new HttpMessageConverterEncoder(converter);
|
||||
}
|
||||
|
||||
static HttpMessageConverterDecoder asDecoder(HttpMessageConverter<Object> converter) {
|
||||
return new HttpMessageConverterDecoder(converter);
|
||||
}
|
||||
|
||||
|
||||
private static class HttpMessageConverterEncoder implements Encoder<Object> {
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@@ -29,7 +31,7 @@ import org.springframework.web.client.RestClient;
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.3
|
||||
*/
|
||||
public interface RestClientGraphQlClient extends WebGraphQlClient {
|
||||
public interface HttpSyncGraphQlClient extends GraphQlClient {
|
||||
|
||||
|
||||
@Override
|
||||
@@ -37,17 +39,17 @@ public interface RestClientGraphQlClient extends WebGraphQlClient {
|
||||
|
||||
|
||||
/**
|
||||
* Create an {@link RestClientGraphQlClient} that uses the given {@link RestClient}.
|
||||
* Create an {@link HttpSyncGraphQlClient} that uses the given {@link RestClient}.
|
||||
*/
|
||||
static RestClientGraphQlClient create(RestClient client) {
|
||||
static HttpSyncGraphQlClient create(RestClient client) {
|
||||
return builder(client.mutate()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a builder to initialize an {@link RestClientGraphQlClient} with.
|
||||
* Return a builder to initialize an {@link HttpSyncGraphQlClient} with.
|
||||
*/
|
||||
static Builder<?> builder() {
|
||||
return new DefaultRestClientGraphQlClientBuilder();
|
||||
return new DefaultSyncHttpGraphQlClientBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,14 +65,40 @@ public interface RestClientGraphQlClient extends WebGraphQlClient {
|
||||
* to mutate and customize further through the returned builder.
|
||||
*/
|
||||
static Builder<?> builder(RestClient.Builder builder) {
|
||||
return new DefaultRestClientGraphQlClientBuilder(builder);
|
||||
return new DefaultSyncHttpGraphQlClientBuilder(builder);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builder for the GraphQL over HTTP client.
|
||||
*/
|
||||
interface Builder<B extends Builder<B>> extends WebGraphQlClient.Builder<B> {
|
||||
interface Builder<B extends Builder<B>> extends GraphQlClient.SyncBuilder<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 message converters for all JSON encoding and decoding needs.
|
||||
@@ -93,7 +121,7 @@ public interface RestClientGraphQlClient extends WebGraphQlClient {
|
||||
* Build the {@code RestClientGraphQlClient} instance.
|
||||
*/
|
||||
@Override
|
||||
RestClientGraphQlClient build();
|
||||
HttpSyncGraphQlClient build();
|
||||
|
||||
}
|
||||
|
||||
@@ -16,25 +16,20 @@
|
||||
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.GraphQlResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
|
||||
/**
|
||||
* Transport to execute GraphQL requests over HTTP via {@link RestClient}.
|
||||
* Transport for executing GraphQL over HTTP requests via {@link RestClient}.
|
||||
*
|
||||
* <p>Supports only single-response requests over HTTP POST. For subscriptions,
|
||||
* see {@link WebSocketGraphQlTransport} and {@link RSocketGraphQlTransport}.
|
||||
@@ -42,7 +37,7 @@ import org.springframework.web.client.RestClient;
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.3
|
||||
*/
|
||||
final class RestClientGraphQlTransport implements GraphQlTransport {
|
||||
final class HttpSyncGraphQlTransport implements SyncGraphQlTransport {
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> MAP_TYPE = new ParameterizedTypeReference<>() {};
|
||||
|
||||
@@ -51,14 +46,11 @@ final class RestClientGraphQlTransport implements GraphQlTransport {
|
||||
|
||||
private final MediaType contentType;
|
||||
|
||||
private final Scheduler scheduler;
|
||||
|
||||
|
||||
RestClientGraphQlTransport(RestClient restClient, @Nullable Scheduler scheduler) {
|
||||
HttpSyncGraphQlTransport(RestClient restClient) {
|
||||
Assert.notNull(restClient, "RestClient is required");
|
||||
this.restClient = restClient;
|
||||
this.contentType = initContentType(restClient);
|
||||
this.scheduler = (scheduler!= null ? scheduler : Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
private static MediaType initContentType(RestClient webClient) {
|
||||
@@ -70,21 +62,14 @@ final class RestClientGraphQlTransport implements GraphQlTransport {
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<GraphQlResponse> execute(GraphQlRequest request) {
|
||||
return Mono
|
||||
.fromCallable(() -> this.restClient.post()
|
||||
.contentType(this.contentType)
|
||||
.accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_GRAPHQL_RESPONSE)
|
||||
.body(request.toMap())
|
||||
.retrieve()
|
||||
.body(MAP_TYPE))
|
||||
.map(responseMap -> (GraphQlResponse) new ResponseMapGraphQlResponse(responseMap))
|
||||
.subscribeOn(this.scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GraphQlResponse> executeSubscription(GraphQlRequest request) {
|
||||
throw new UnsupportedOperationException("Subscriptions not supported");
|
||||
public GraphQlResponse execute(GraphQlRequest request) {
|
||||
Map<String, Object> body = this.restClient.post()
|
||||
.contentType(this.contentType)
|
||||
.accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_GRAPHQL_RESPONSE)
|
||||
.body(request.toMap())
|
||||
.retrieve()
|
||||
.body(MAP_TYPE);
|
||||
return new ResponseMapGraphQlResponse(body != null ? body : Collections.emptyMap());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -97,7 +97,8 @@ public class GraphQlClientBuilderTests extends GraphQlClientTestSupport {
|
||||
|
||||
// Mutate
|
||||
savedAttributes.clear();
|
||||
client = client.mutate().interceptors(interceptors -> interceptors.add(0, changingInterceptor)).build();
|
||||
builder = (GraphQlClient.Builder<?>) client.mutate();
|
||||
client = builder.interceptors(interceptors -> interceptors.add(0, changingInterceptor)).build();
|
||||
response = client.document(DOCUMENT).attribute(name, value).execute().block(TIMEOUT);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
|
||||
import graphql.ExecutionResultImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.execution.MockExecutionGraphQlService;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.WebGraphQlInterceptor;
|
||||
import org.springframework.graphql.server.WebGraphQlRequest;
|
||||
import org.springframework.graphql.server.webflux.GraphQlHttpHandler;
|
||||
import org.springframework.graphql.support.DocumentSource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.test.web.reactive.server.HttpHandlerConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||
|
||||
/**
|
||||
* Tests for the {@link HttpSyncGraphQlClient} builder performing requests to a
|
||||
* {@link GraphQlHttpHandler} with a {@link MockExecutionGraphQlService} that
|
||||
* always returns an empty GraphQL response. The main goal however is to capture
|
||||
* the WebInput on the server side through a {@link WebGraphQlInterceptor}.
|
||||
*
|
||||
* <p>The equivalent of {@link WebGraphQlClientBuilderTests} but for
|
||||
* {@link HttpSyncGraphQlClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class HttpSyncGraphQlClientBuilderTests {
|
||||
|
||||
private static final String DOCUMENT = "{ Query }";
|
||||
|
||||
private final ClientBuilderSetup setup = new ClientBuilderSetup();
|
||||
|
||||
|
||||
@Test
|
||||
void mutateUrlHeaders() {
|
||||
|
||||
String url = "/graphql-one";
|
||||
|
||||
// Original
|
||||
HttpSyncGraphQlClient.Builder<?> builder = this.setup.initBuilder()
|
||||
.url(url)
|
||||
.headers(headers -> headers.add("h", "one"));
|
||||
|
||||
HttpSyncGraphQlClient client = builder.build();
|
||||
client.document(DOCUMENT).executeSync();
|
||||
|
||||
WebGraphQlRequest request = this.setup.getActualRequest();
|
||||
assertThat(request.getUri().toString()).isEqualTo(url);
|
||||
assertThat(request.getHeaders().get("h")).containsExactly("one");
|
||||
|
||||
// Mutate to add header value
|
||||
builder = client.mutate().headers(headers -> headers.add("h", "two"));
|
||||
client = builder.build();
|
||||
client.document(DOCUMENT).executeSync();
|
||||
assertThat(setup.getActualRequest().getHeaders().get("h")).containsExactly("one", "two");
|
||||
|
||||
// Mutate to replace header
|
||||
builder = client.mutate().header("h", "three", "four");
|
||||
client = builder.build();
|
||||
client.document(DOCUMENT).executeSync();
|
||||
|
||||
request = this.setup.getActualRequest();
|
||||
assertThat(request.getUri().toString()).isEqualTo(url);
|
||||
assertThat(request.getHeaders().get("h")).containsExactly("three", "four");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mutateWebTestClientViaConsumer() {
|
||||
|
||||
// Original header value
|
||||
HttpSyncGraphQlClient.Builder<?> builder = this.setup.initBuilder()
|
||||
.restClient(testClientBuilder -> testClientBuilder.defaultHeaders(h -> h.add("h", "one")));
|
||||
|
||||
HttpSyncGraphQlClient client = builder.build();
|
||||
client.document(DOCUMENT).executeSync();
|
||||
assertThat(setup.getActualRequest().getHeaders().get("h")).containsExactly("one");
|
||||
|
||||
// Mutate to add header value
|
||||
HttpSyncGraphQlClient.Builder<?> builder2 = client.mutate()
|
||||
.restClient(testClientBuilder -> testClientBuilder.defaultHeaders(h -> h.add("h", "two")));
|
||||
|
||||
client = builder2.build();
|
||||
client.document(DOCUMENT).executeSync();
|
||||
assertThat(setup.getActualRequest().getHeaders().get("h")).containsExactly("one", "two");
|
||||
|
||||
// Mutate to replace header
|
||||
HttpSyncGraphQlClient.Builder<?> builder3 = client.mutate()
|
||||
.restClient(testClientBuilder -> testClientBuilder.defaultHeader("h", "three"));
|
||||
|
||||
client = builder3.build();
|
||||
client.document(DOCUMENT).executeSync();
|
||||
assertThat(setup.getActualRequest().getHeaders().get("h")).containsExactly("three");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mutateDocumentSource() {
|
||||
|
||||
DocumentSource documentSource = name -> name.equals("name") ?
|
||||
Mono.just(DOCUMENT) : Mono.error(new IllegalArgumentException());
|
||||
|
||||
// Original
|
||||
HttpSyncGraphQlClient.Builder<?> builder = this.setup.initBuilder().documentSource(documentSource);
|
||||
HttpSyncGraphQlClient client = builder.build();
|
||||
client.documentName("name").executeSync();
|
||||
|
||||
WebGraphQlRequest request = this.setup.getActualRequest();
|
||||
assertThat(request.getDocument()).isEqualTo(DOCUMENT);
|
||||
|
||||
// Mutate
|
||||
client = client.mutate().build();
|
||||
client.documentName("name").executeSync();
|
||||
|
||||
request = this.setup.getActualRequest();
|
||||
assertThat(request.getDocument()).isEqualTo(DOCUMENT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void urlEncoding() {
|
||||
|
||||
HttpSyncGraphQlClient client = this.setup.initBuilder().url("/graphql one").build();
|
||||
client.document(DOCUMENT).executeSync();
|
||||
|
||||
assertThat(this.setup.getActualRequest().getUri().toString()).isEqualTo("/graphql%20one");
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentTypeDefault() {
|
||||
|
||||
this.setup.initBuilder().build().document(DOCUMENT).executeSync();
|
||||
|
||||
WebGraphQlRequest request = this.setup.getActualRequest();
|
||||
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentTypeOverride() {
|
||||
MediaType testMediaType = new MediaType("application", "graphql-request+json");
|
||||
|
||||
setup.initBuilder()
|
||||
.header(HttpHeaders.CONTENT_TYPE, "application/graphql-request+json").build()
|
||||
.document(DOCUMENT).executeSync();
|
||||
|
||||
WebGraphQlRequest request = this.setup.getActualRequest();
|
||||
assertThat(request.getHeaders().getContentType()).isEqualTo(testMediaType);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void codecConfigurerRegistersJsonPathMappingProvider() {
|
||||
|
||||
TestJackson2JsonConverter testConverter = new TestJackson2JsonConverter();
|
||||
|
||||
HttpSyncGraphQlClient.Builder<?> builder = this.setup.initBuilder();
|
||||
builder.messageConverters(converters -> converters.add(0, testConverter));
|
||||
|
||||
String document = "{me {name}}";
|
||||
MovieCharacter character = MovieCharacter.create("Luke Skywalker");
|
||||
this.setup.getGraphQlService().setResponse(document,
|
||||
ExecutionResultImpl.newExecutionResult()
|
||||
.data(Collections.singletonMap("me", character))
|
||||
.build());
|
||||
|
||||
HttpSyncGraphQlClient client = builder.build();
|
||||
ClientGraphQlResponse response = client.document(document).executeSync();
|
||||
|
||||
testConverter.resetLastValue();
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.field("me").toEntity(MovieCharacter.class).getName()).isEqualTo("Luke Skywalker");
|
||||
|
||||
Object lastValue = testConverter.getLastValue();
|
||||
assertThat(lastValue).isEqualTo(character);
|
||||
}
|
||||
|
||||
|
||||
private static class ClientBuilderSetup {
|
||||
|
||||
private final MockExecutionGraphQlService graphQlService = new MockExecutionGraphQlService();
|
||||
|
||||
@Nullable
|
||||
private WebGraphQlRequest graphQlRequest;
|
||||
|
||||
public ClientBuilderSetup() {
|
||||
this.graphQlService.setDefaultResponse("{}");
|
||||
}
|
||||
|
||||
public MockExecutionGraphQlService getGraphQlService() {
|
||||
return this.graphQlService;
|
||||
}
|
||||
|
||||
public WebGraphQlRequest getActualRequest() {
|
||||
Assert.state(this.graphQlRequest != null, "No saved WebGraphQlRequest");
|
||||
return this.graphQlRequest;
|
||||
}
|
||||
|
||||
public HttpSyncGraphQlClient.Builder<?> initBuilder() {
|
||||
HttpHandler httpHandler = initServer();
|
||||
ClientHttpRequestFactory requestFactory = new HttpHandlerClientHttpRequestFactory(httpHandler);
|
||||
return HttpSyncGraphQlClient.builder(RestClient.builder().requestFactory(requestFactory));
|
||||
}
|
||||
|
||||
private HttpHandler initServer() {
|
||||
GraphQlHttpHandler handler = new GraphQlHttpHandler(WebGraphQlHandler.builder(this.graphQlService)
|
||||
.interceptor((request, chain) -> {
|
||||
this.graphQlRequest = request;
|
||||
return chain.next(graphQlRequest);
|
||||
})
|
||||
.build());
|
||||
RouterFunction<ServerResponse> routerFunction = route().POST("/**", handler::handleRequest).build();
|
||||
return RouterFunctions.toHttpHandler(routerFunction, HandlerStrategies.withDefaults());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static final class HttpHandlerClientHttpRequestFactory implements ClientHttpRequestFactory {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
private HttpHandlerClientHttpRequestFactory(HttpHandler httpHandler) {
|
||||
ClientHttpConnector connector = new HttpHandlerConnector(httpHandler);
|
||||
this.webClient = WebClient.builder().clientConnector(connector).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
|
||||
return new MockClientHttpRequest(httpMethod, uri) {
|
||||
@Override
|
||||
protected ClientHttpResponse executeInternal() {
|
||||
return getClientHttpResponse(httpMethod, uri, getHeaders(), getBodyAsBytes());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ClientHttpResponse getClientHttpResponse(
|
||||
HttpMethod httpMethod, URI uri, HttpHeaders requestHeaders, byte[] requestBody) {
|
||||
|
||||
ResponseEntity<byte[]> entity = this.webClient.method(httpMethod).uri(uri)
|
||||
.headers(headers -> headers.putAll(requestHeaders))
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.toEntity(byte[].class)
|
||||
.block();
|
||||
|
||||
byte[] body = (entity.getBody() != null ? entity.getBody() : new byte[0]);
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(body, entity.getStatusCode());
|
||||
response.getHeaders().putAll(entity.getHeaders());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class TestJackson2JsonConverter extends MappingJackson2HttpMessageConverter {
|
||||
|
||||
@Nullable
|
||||
private Object lastValue;
|
||||
|
||||
@Nullable
|
||||
Object getLastValue() {
|
||||
return this.lastValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
this.lastValue = super.read(type, contextClass, inputMessage);
|
||||
return this.lastValue;
|
||||
}
|
||||
|
||||
void resetLastValue() {
|
||||
this.lastValue = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package org.springframework.graphql.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
@@ -42,25 +40,14 @@ import org.springframework.graphql.server.webflux.GraphQlHttpHandler;
|
||||
import org.springframework.graphql.server.webflux.GraphQlWebSocketHandler;
|
||||
import org.springframework.graphql.support.DocumentSource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.codec.ClientCodecConfigurer;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.test.web.reactive.server.HttpHandlerConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
@@ -75,15 +62,19 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||
|
||||
/**
|
||||
* Tests for the builders of Web {@code GraphQlClient} extensions, using a
|
||||
* {@link WebGraphQlInterceptor} to capture the WebInput on the server
|
||||
* side, and optionally returning a mock response, or an empty response.
|
||||
*
|
||||
* Tests for the builders of web {@code GraphQlClient}'s performing requests as follows:
|
||||
* <ul>
|
||||
* <li>{@link HttpGraphQlClient} via {@link HttpHandlerConnector} to {@link GraphQlHttpHandler}
|
||||
* <li>{@link WebSocketGraphQlClient} via a {@link TestWebSocketConnection} to {@link GraphQlWebSocketHandler}
|
||||
* </ul>
|
||||
*
|
||||
* <p>GraphQL requests are handled with a {@link MockExecutionGraphQlService} that
|
||||
* always returns an empty GraphQL response. The main goal however is to capture
|
||||
* the WebInput on the server side through a {@link WebGraphQlInterceptor}.
|
||||
*
|
||||
* <p>See also {@link HttpSyncGraphQlClientBuilderTests} for the same tests with
|
||||
* {@link HttpSyncGraphQlClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class WebGraphQlClientBuilderTests {
|
||||
@@ -94,25 +85,25 @@ public class WebGraphQlClientBuilderTests {
|
||||
|
||||
|
||||
public static Stream<ClientBuilderSetup> argumentSource() {
|
||||
return Stream.of(new HttpBuilderSetup(), new RestClientBuilderSetup(), new WebSocketBuilderSetup());
|
||||
return Stream.of(new HttpBuilderSetup(), new WebSocketBuilderSetup());
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void mutateUrlHeaders(ClientBuilderSetup builderSetup) {
|
||||
void mutateUrlHeaders(ClientBuilderSetup setup) {
|
||||
|
||||
String url = "/graphql-one";
|
||||
|
||||
// Original
|
||||
WebGraphQlClient.Builder<?> builder = builderSetup.initBuilder()
|
||||
WebGraphQlClient.Builder<?> builder = setup.initBuilder()
|
||||
.url(url)
|
||||
.headers(headers -> headers.add("h", "one"));
|
||||
|
||||
WebGraphQlClient client = builder.build();
|
||||
client.document(DOCUMENT).execute().block(TIMEOUT);
|
||||
|
||||
WebGraphQlRequest request = builderSetup.getActualRequest();
|
||||
WebGraphQlRequest request = setup.getActualRequest();
|
||||
assertThat(request.getUri().toString()).isEqualTo(url);
|
||||
assertThat(request.getHeaders().get("h")).containsExactly("one");
|
||||
|
||||
@@ -120,14 +111,14 @@ public class WebGraphQlClientBuilderTests {
|
||||
builder = client.mutate().headers(headers -> headers.add("h", "two"));
|
||||
client = builder.build();
|
||||
client.document(DOCUMENT).execute().block(TIMEOUT);
|
||||
assertThat(builderSetup.getActualRequest().getHeaders().get("h")).containsExactly("one", "two");
|
||||
assertThat(setup.getActualRequest().getHeaders().get("h")).containsExactly("one", "two");
|
||||
|
||||
// Mutate to replace header
|
||||
builder = client.mutate().header("h", "three", "four");
|
||||
client = builder.build();
|
||||
client.document(DOCUMENT).execute().block(TIMEOUT);
|
||||
|
||||
request = builderSetup.getActualRequest();
|
||||
request = setup.getActualRequest();
|
||||
assertThat(request.getUri().toString()).isEqualTo(url);
|
||||
assertThat(request.getHeaders().get("h")).containsExactly("three", "four");
|
||||
}
|
||||
@@ -163,35 +154,35 @@ public class WebGraphQlClientBuilderTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void mutateDocumentSource(ClientBuilderSetup builderSetup) {
|
||||
void mutateDocumentSource(ClientBuilderSetup setup) {
|
||||
|
||||
DocumentSource documentSource = name -> name.equals("name") ?
|
||||
Mono.just(DOCUMENT) : Mono.error(new IllegalArgumentException());
|
||||
|
||||
// Original
|
||||
WebGraphQlClient.Builder<?> builder = builderSetup.initBuilder().documentSource(documentSource);
|
||||
WebGraphQlClient.Builder<?> builder = setup.initBuilder().documentSource(documentSource);
|
||||
WebGraphQlClient client = builder.build();
|
||||
client.documentName("name").execute().block(TIMEOUT);
|
||||
|
||||
WebGraphQlRequest request = builderSetup.getActualRequest();
|
||||
WebGraphQlRequest request = setup.getActualRequest();
|
||||
assertThat(request.getDocument()).isEqualTo(DOCUMENT);
|
||||
|
||||
// Mutate
|
||||
client = client.mutate().build();
|
||||
client.documentName("name").execute().block(TIMEOUT);
|
||||
|
||||
request = builderSetup.getActualRequest();
|
||||
request = setup.getActualRequest();
|
||||
assertThat(request.getDocument()).isEqualTo(DOCUMENT);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void urlEncoding(ClientBuilderSetup builderSetup) {
|
||||
void urlEncoding(ClientBuilderSetup setup) {
|
||||
|
||||
WebGraphQlClient client = builderSetup.initBuilder().url("/graphql one").build();
|
||||
WebGraphQlClient client = setup.initBuilder().url("/graphql one").build();
|
||||
client.document(DOCUMENT).execute().block(TIMEOUT);
|
||||
|
||||
assertThat(builderSetup.getActualRequest().getUri().toString()).isEqualTo("/graphql%20one");
|
||||
assertThat(setup.getActualRequest().getUri().toString()).isEqualTo("/graphql%20one");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -219,22 +210,16 @@ public class WebGraphQlClientBuilderTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void codecConfigurerRegistersJsonPathMappingProvider(ClientBuilderSetup builderSetup) {
|
||||
void codecConfigurerRegistersJsonPathMappingProvider(ClientBuilderSetup setup) {
|
||||
|
||||
TestJackson2JsonDecoder testDecoder = new TestJackson2JsonDecoder();
|
||||
TestJackson2JsonConverter testConverter = new TestJackson2JsonConverter();
|
||||
|
||||
WebGraphQlClient.Builder<?> builder = builderSetup.initBuilder();
|
||||
if (builder instanceof RestClientGraphQlClient.Builder<?> restClientBuilder) {
|
||||
restClientBuilder.messageConverters(converters -> converters.add(0, testConverter));
|
||||
}
|
||||
else {
|
||||
builder.codecConfigurer(codecConfigurer -> codecConfigurer.customCodecs().register(testDecoder));
|
||||
}
|
||||
WebGraphQlClient.Builder<?> builder = setup.initBuilder();
|
||||
builder.codecConfigurer(codecConfigurer -> codecConfigurer.customCodecs().register(testDecoder));
|
||||
|
||||
String document = "{me {name}}";
|
||||
MovieCharacter character = MovieCharacter.create("Luke Skywalker");
|
||||
builderSetup.getGraphQlService().setResponse(document,
|
||||
setup.getGraphQlService().setResponse(document,
|
||||
ExecutionResultImpl.newExecutionResult()
|
||||
.data(Collections.singletonMap("me", character))
|
||||
.build());
|
||||
@@ -243,34 +228,28 @@ public class WebGraphQlClientBuilderTests {
|
||||
ClientGraphQlResponse response = client.document(document).execute().block(TIMEOUT);
|
||||
|
||||
testDecoder.resetLastValue();
|
||||
testConverter.resetLastValue();
|
||||
assertThat(testDecoder.getLastValue()).isNull();
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.field("me").toEntity(MovieCharacter.class).getName()).isEqualTo("Luke Skywalker");
|
||||
|
||||
Object lastValue = (builder instanceof RestClientGraphQlClient.Builder<?> ?
|
||||
testConverter.getLastValue() : testDecoder.getLastValue());
|
||||
|
||||
assertThat(lastValue).isEqualTo(character);
|
||||
}
|
||||
|
||||
@Test
|
||||
void attributes() {
|
||||
|
||||
HttpBuilderSetup builderSetup = new HttpBuilderSetup();
|
||||
HttpBuilderSetup setup = new HttpBuilderSetup();
|
||||
|
||||
builderSetup.initBuilder().url("/graphql-one").headers(headers -> headers.add("h", "one")).build()
|
||||
setup.initBuilder().url("/graphql-one").headers(headers -> headers.add("h", "one")).build()
|
||||
.document(DOCUMENT)
|
||||
.attribute("id", 123)
|
||||
.execute()
|
||||
.block(TIMEOUT);
|
||||
|
||||
assertThat(builderSetup.getClientAttributes()).containsEntry("id", 123);
|
||||
assertThat(setup.getClientAttributes()).containsEntry("id", 123);
|
||||
}
|
||||
|
||||
|
||||
private interface ClientBuilderSetup {
|
||||
interface ClientBuilderSetup {
|
||||
|
||||
MockExecutionGraphQlService getGraphQlService();
|
||||
|
||||
@@ -315,19 +294,7 @@ public class WebGraphQlClientBuilderTests {
|
||||
}
|
||||
|
||||
|
||||
private abstract static class AbstractHttpBuilderSetup extends AbstractBuilderSetup {
|
||||
|
||||
protected WebClient.Builder initWebClientBuilder() {
|
||||
GraphQlHttpHandler handler = new GraphQlHttpHandler(webGraphQlHandler());
|
||||
RouterFunction<ServerResponse> routerFunction = route().POST("/**", handler::handleRequest).build();
|
||||
HttpHandler httpHandler = RouterFunctions.toHttpHandler(routerFunction, HandlerStrategies.withDefaults());
|
||||
return WebClient.builder().clientConnector(new HttpHandlerConnector(httpHandler));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class HttpBuilderSetup extends AbstractHttpBuilderSetup {
|
||||
private static class HttpBuilderSetup extends AbstractBuilderSetup {
|
||||
|
||||
private final Map<String, Object> clientAttributes = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -347,18 +314,12 @@ public class WebGraphQlClientBuilderTests {
|
||||
return next.exchange(request);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class RestClientBuilderSetup extends AbstractHttpBuilderSetup {
|
||||
|
||||
@Override
|
||||
public RestClientGraphQlClient.Builder<?> initBuilder() {
|
||||
WebClient webClient = initWebClientBuilder().build();
|
||||
WebClientHttpRequestFactoryAdapter requestFactory = new WebClientHttpRequestFactoryAdapter(webClient);
|
||||
return RestClientGraphQlClient.builder(RestClient.builder().requestFactory(requestFactory));
|
||||
private WebClient.Builder initWebClientBuilder() {
|
||||
GraphQlHttpHandler handler = new GraphQlHttpHandler(webGraphQlHandler());
|
||||
RouterFunction<ServerResponse> routerFunction = route().POST("/**", handler::handleRequest).build();
|
||||
HttpHandler httpHandler = RouterFunctions.toHttpHandler(routerFunction, HandlerStrategies.withDefaults());
|
||||
return WebClient.builder().clientConnector(new HttpHandlerConnector(httpHandler));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -374,37 +335,6 @@ public class WebGraphQlClientBuilderTests {
|
||||
}
|
||||
|
||||
|
||||
private record WebClientHttpRequestFactoryAdapter(WebClient webClient) implements ClientHttpRequestFactory {
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
|
||||
return new MockClientHttpRequest(httpMethod, uri) {
|
||||
@Override
|
||||
protected ClientHttpResponse executeInternal() {
|
||||
return getClientHttpResponse(httpMethod, uri, getHeaders(), getBodyAsBytes());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ClientHttpResponse getClientHttpResponse(
|
||||
HttpMethod httpMethod, URI uri, HttpHeaders requestHeaders, byte[] requestBody) {
|
||||
|
||||
ResponseEntity<byte[]> entity = this.webClient.method(httpMethod).uri(uri)
|
||||
.headers(headers -> headers.putAll(requestHeaders))
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.toEntity(byte[].class)
|
||||
.block();
|
||||
|
||||
byte[] body = (entity.getBody() != null ? entity.getBody() : new byte[0]);
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(body, entity.getStatusCode());
|
||||
response.getHeaders().putAll(entity.getHeaders());
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class TestJackson2JsonDecoder extends Jackson2JsonDecoder {
|
||||
|
||||
@Nullable
|
||||
@@ -429,29 +359,4 @@ public class WebGraphQlClientBuilderTests {
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class TestJackson2JsonConverter extends MappingJackson2HttpMessageConverter {
|
||||
|
||||
@Nullable
|
||||
private Object lastValue;
|
||||
|
||||
@Nullable
|
||||
Object getLastValue() {
|
||||
return this.lastValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
this.lastValue = super.read(type, contextClass, inputMessage);
|
||||
return this.lastValue;
|
||||
}
|
||||
|
||||
void resetLastValue() {
|
||||
this.lastValue = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user