DocumentSource is async and supports caching

See gh-10
This commit is contained in:
rstoyanchev
2022-03-01 17:00:10 +00:00
parent 087202d9cb
commit 328d5c2b3e
16 changed files with 465 additions and 177 deletions

View File

@@ -29,6 +29,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -69,20 +70,19 @@ final class DefaultGraphQlClient implements GraphQlClient {
@Override
public RequestSpec document(String document) {
return new DefaultRequestSpec(document, this.transport, this.jsonPathConfig);
return new DefaultRequestSpec(Mono.just(document), this.transport, this.jsonPathConfig);
}
@Override
public RequestSpec documentName(String name) {
String document = this.documentSource.getDocument(name);
Assert.notNull(document, "Failed to find document for name: '" + name + "'");
Mono<String> document = this.documentSource.getDocument(name);
return new DefaultRequestSpec(document, this.transport, this.jsonPathConfig);
}
private static final class DefaultRequestSpec implements RequestSpec {
private final String document;
private final Mono<String> documentMono;
@Nullable
private String operationName;
@@ -93,9 +93,9 @@ final class DefaultGraphQlClient implements GraphQlClient {
private final Configuration jsonPathConfig;
DefaultRequestSpec(String document, GraphQlTransport transport, Configuration jsonPathConfig) {
Assert.hasText(document, "'document' is required");
this.document = document;
DefaultRequestSpec(Mono<String> documentMono, GraphQlTransport transport, Configuration jsonPathConfig) {
Assert.notNull(documentMono, "'documentMono' is required");
this.documentMono = documentMono;
this.transport = transport;
this.jsonPathConfig = jsonPathConfig;
}
@@ -114,18 +114,21 @@ final class DefaultGraphQlClient implements GraphQlClient {
@Override
public Mono<ResponseSpec> execute() {
return this.transport.execute(createRequest())
return getRequestMono()
.flatMap(this.transport::execute)
.map(payload -> new DefaultResponseSpec(payload, this.jsonPathConfig));
}
@Override
public Flux<ResponseSpec> executeSubscription() {
return this.transport.executeSubscription(createRequest())
return getRequestMono()
.flatMapMany(this.transport::executeSubscription)
.map(payload -> new DefaultResponseSpec(payload, this.jsonPathConfig));
}
private GraphQlRequest createRequest() {
return new GraphQlRequest(this.document, this.operationName, this.variables);
private Mono<GraphQlRequest> getRequestMono() {
return this.documentMono.map(document ->
new GraphQlRequest(document, this.operationName, this.variables));
}
}

View File

@@ -19,6 +19,8 @@ import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
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;

View File

@@ -23,6 +23,8 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.graphql.support.ResourceDocumentSource;
import org.springframework.lang.Nullable;
/**

View File

@@ -23,6 +23,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.support.MapExecutionResult;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
@@ -80,7 +81,7 @@ public class HttpGraphQlTransport implements GraphQlTransport {
.bodyValue(request.toMap())
.retrieve()
.bodyToMono(MAP_TYPE)
.map(MapExecutionResult::new);
.map(MapExecutionResult::from);
}
@Override

View File

@@ -1,115 +0,0 @@
/*
* 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.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils;
/**
* {@link DocumentSource} that looks under a set of locations for a
* {@link Resource} with the document name and a list of configured extensions.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public class ResourceDocumentSource implements DocumentSource {
private static final List<String> FILE_EXTENSIONS = Arrays.asList(".graphql", ".gql");
private final List<Resource> locations;
private final List<String> extensions;
/**
* Default constructor to look under {@code graphql/} on the classpath for
* resources with extensions ".graphql" and ".gql".
*/
public ResourceDocumentSource() {
this(Collections.singletonList(new ClassPathResource("graphql/")));
}
/**
* Constructor with custom locations with extensions ".graphql" and ".gql".
*/
public ResourceDocumentSource(List<Resource> locations) {
this(locations, FILE_EXTENSIONS);
}
/**
* Constructor with given locations and extensions.
*/
public ResourceDocumentSource(List<Resource> locations, List<String> extensions) {
this.locations = new ArrayList<>(locations);
this.extensions = new ArrayList<>(extensions);
}
/**
* Return the configured locations.
*/
public List<Resource> getLocations() {
return this.locations;
}
/**
* Return the configured extensions.
*/
public List<String> getExtensions() {
return this.extensions;
}
@Override
public String getDocument(String name) {
return this.locations.stream()
.flatMap(location -> this.extensions.stream().map(ext -> getRelativeResource(location, name, ext)))
.filter(Resource::exists)
.findFirst()
.map(resource -> {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
FileCopyUtils.copy(resource.getInputStream(), out);
return new String(out.toByteArray(), StandardCharsets.UTF_8);
}
catch (IOException ex) {
throw new IllegalArgumentException(
"Found resource: " + resource.getDescription() + " but failed to read it", ex);
}
})
.orElse(null);
}
private Resource getRelativeResource(Resource location, String name, String ext) {
try {
return location.createRelative(name + ext);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}

View File

@@ -36,6 +36,8 @@ import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.support.MapExecutionResult;
import org.springframework.graphql.support.MapGraphQlError;
import org.springframework.graphql.web.webflux.GraphQlWebSocketMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.ClientCodecConfigurer;
@@ -614,7 +616,9 @@ public final class WebSocketGraphQlTransport implements GraphQlTransport {
return;
}
ExecutionResult result = new MapExecutionResult(message.getPayload());
Map<String, Object> resultMap = message.getPayloadOrDefault(Collections.emptyMap());
ExecutionResult result = MapExecutionResult.from(resultMap);
Sinks.EmitResult emitResult = (sink != null ? sink.tryEmitValue(result) : streamingSink.tryEmitNext(result));
if (emitResult.isFailure()) {
// Just log: cannot overflow, is serialized, and cancel is handled in doOnCancel
@@ -640,15 +644,15 @@ public final class WebSocketGraphQlTransport implements GraphQlTransport {
return;
}
List<Map<String, Object>> payload = message.getPayload();
List<Map<String, Object>> payload = message.getPayloadOrDefault(Collections.emptyList());
Sinks.EmitResult emitResult;
if (sink != null) {
ExecutionResult result = new MapExecutionResult(Collections.singletonMap("errors", payload));
ExecutionResult result = MapExecutionResult.forErrorsOnly(payload);
emitResult = sink.tryEmitValue(result);
}
else {
List<GraphQLError> graphQLErrors = MapGraphQlError.fromMapList(payload);
List<GraphQLError> graphQLErrors = MapGraphQlError.from(payload);
Exception ex = new SubscriptionErrorException(graphQLErrors);
emitResult = streamingSink.tryEmitError(ex);
}

View File

@@ -0,0 +1,82 @@
/*
* 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.support;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import reactor.core.publisher.Mono;
/**
* Base class for {@link DocumentSource} implementations providing support for
* caching loaded documents.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public class CachingDocumentSource implements DocumentSource {
private final DocumentSource delegate;
private boolean cacheEnabled = true;
private final Map<String, Mono<String>> documentCache = new ConcurrentHashMap<>();
/**
* Constructor with the {@code DocumentSource} to actually load documents.
*/
public CachingDocumentSource(DocumentSource delegate) {
this.delegate = delegate;
}
/**
* Enable or disable caching of resolved documents.
* <p>By default, set to {@code true}.
* @param cacheEnabled enable if {@code true} and disable if {@code false}
*/
public void setCacheEnabled(boolean cacheEnabled) {
this.cacheEnabled = cacheEnabled;
if (!cacheEnabled) {
this.documentCache.clear();
}
}
/**
* Whether {@link #setCacheEnabled(boolean) caching} is enabled.
*/
public boolean isCacheEnabled() {
return cacheEnabled;
}
@Override
public Mono<String> getDocument(String name) {
return (isCacheEnabled() ?
this.documentCache.computeIfAbsent(name, k -> this.delegate.getDocument(name).cache()) :
this.delegate.getDocument(name));
}
/**
* Remove all entries from the document cache.
*/
public void clearCache() {
this.documentCache.clear();
}
}

View File

@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.client;
package org.springframework.graphql.support;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Mono;
/**
* Strategy to locate a GraphQL document identified by name.
* Strategy to locate a GraphQL document by a name.
*
* @author Rossen Stoyanchev
* @since 1.0.0
@@ -28,9 +28,8 @@ public interface DocumentSource {
/**
* Return the document that matches the given name.
* @param name the name to use for the lookup
* @return the document, or {@code null}
* @return {@code Mono} that provides the document or returns an error
*/
@Nullable
String getDocument(String name);
Mono<String> getDocument(String name);
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.graphql.client;
package org.springframework.graphql.support;
import java.util.Collections;
import java.util.List;
@@ -24,7 +24,7 @@ import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import graphql.GraphQLError;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Implementation of {@link ExecutionResult} backed by a {@link Map}.
@@ -32,16 +32,18 @@ import org.springframework.lang.Nullable;
* @author Rossen Stoyanchev
* @since 1.0.0
*/
final class MapExecutionResult implements ExecutionResult {
public final class MapExecutionResult implements ExecutionResult {
private final Map<String, Object> map;
private final Map<String, Object> resultMap;
private final List<GraphQLError> errors;
MapExecutionResult(@Nullable Map<String, Object> map) {
this.map = (map != null ? map : Collections.emptyMap());
this.errors = MapGraphQlError.fromResultMap(map);
@SuppressWarnings("unchecked")
private MapExecutionResult(Map<String, Object> resultMap) {
Assert.notNull(resultMap, "'resultMap' is required");
this.resultMap = resultMap;
this.errors = MapGraphQlError.from((List<Map<String, Object>>) resultMap.get("errors"));
}
@@ -53,18 +55,18 @@ final class MapExecutionResult implements ExecutionResult {
@SuppressWarnings("unchecked")
@Override
public <T> T getData() {
return (T) this.map.get("data");
return (T) this.resultMap.get("data");
}
@Override
public boolean isDataPresent() {
return (this.map.get("data") != null);
return (this.resultMap.get("data") != null);
}
@SuppressWarnings("unchecked")
@Override
public Map<Object, Object> getExtensions() {
return (Map<Object, Object>) this.map.get("extensions");
return (Map<Object, Object>) this.resultMap.get("extensions");
}
@Override
@@ -74,25 +76,43 @@ final class MapExecutionResult implements ExecutionResult {
@Override
public boolean equals(Object other) {
return (other instanceof MapExecutionResult && this.map.equals(((MapExecutionResult) other).map));
return (other instanceof MapExecutionResult &&
this.resultMap.equals(((MapExecutionResult) other).resultMap));
}
@Override
public int hashCode() {
return this.map.hashCode();
return this.resultMap.hashCode();
}
@Override
public String toString() {
return this.map.toString();
return this.resultMap.toString();
}
/**
* Static factory method to create an instance of this class.
* Create an instance from an {@code ExecutionResult} serialized to map via
* {@link ExecutionResult#toSpecification()}.
*/
public static ExecutionResult forData(@Nullable Map<String, Object> map) {
public static ExecutionResult from(Map<String, Object> map) {
return new MapExecutionResult(map);
}
/**
* Create an {@code ExecutionResult} with a "data" key that returns the
* given map.
*/
public static ExecutionResult forDataOnly(Map<String, Object> map) {
return new MapExecutionResult(Collections.singletonMap("data", map));
}
/**
* Create an {@code ExecutionResult} with an "errors" key that returns the
* given serialized errors.
*/
public static ExecutionResult forErrorsOnly(List<Map<String, Object>> errors) {
return new MapExecutionResult(Collections.singletonMap("errors", errors));
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.graphql.client;
package org.springframework.graphql.support;
import java.util.Collections;
import java.util.List;
@@ -28,6 +28,7 @@ import graphql.language.SourceLocation;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
@@ -37,7 +38,7 @@ import org.springframework.util.CollectionUtils;
* @since 1.0.0
*/
@SuppressWarnings("serial")
final class MapGraphQlError implements GraphQLError {
public final class MapGraphQlError implements GraphQLError {
private final Map<String, Object> errorMap;
@@ -45,6 +46,7 @@ final class MapGraphQlError implements GraphQLError {
private MapGraphQlError(Map<String, Object> errorMap) {
Assert.notNull(errorMap, "'errorMap' is required");
this.errorMap = errorMap;
this.locations = initLocations(errorMap);
}
@@ -64,8 +66,6 @@ final class MapGraphQlError implements GraphQLError {
}
@Override
@Nullable
public String getMessage() {
@@ -119,6 +119,7 @@ final class MapGraphQlError implements GraphQLError {
return GraphqlErrorHelper.toSpecification(this);
}
@SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
@Override
public boolean equals(Object other) {
return GraphqlErrorHelper.equals(this, other);
@@ -136,26 +137,12 @@ final class MapGraphQlError implements GraphQLError {
/**
* Static factory method to create an instance from a list of maps, each
* containing an error.
* Create a list of {@code GraphQlError} instances from the given
* deserialized content.
*/
public static List<GraphQLError> fromMapList(@Nullable List<Map<String, Object>> errorMaps) {
if (CollectionUtils.isEmpty(errorMaps)) {
return Collections.emptyList();
}
return errorMaps.stream().map(MapGraphQlError::new).collect(Collectors.toList());
}
/**
* Static factory method to create an instance from an
* {@link graphql.ExecutionResult} map.
*/
@SuppressWarnings("unchecked")
public static List<GraphQLError> fromResultMap(@Nullable Map<String, Object> map) {
if (map == null) {
return Collections.emptyList();
}
return MapGraphQlError.fromMapList((List<Map<String, Object>>) map.get("errors"));
public static List<GraphQLError> from(@Nullable List<Map<String, Object>> errors) {
errors = (errors != null ? errors : Collections.emptyList());
return errors.stream().map(MapGraphQlError::new).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,128 @@
/*
* 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.support;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils;
/**
* {@link DocumentSource} that looks for a document {@link Resource} under a set
* of locations and trying a number of different file extension.
*
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public class ResourceDocumentSource implements DocumentSource {
/**
* The default file extensions, ".graphql" and ".gql".
*/
public static final List<String> FILE_EXTENSIONS = Arrays.asList(".graphql", ".gql");
private final List<Resource> locations;
private final List<String> extensions;
/**
* Default constructor that sets the location to {@code "classpath:graphql/"}
* and the extensions to ".graphql" and ".gql".
*/
public ResourceDocumentSource() {
this(Collections.singletonList(new ClassPathResource("graphql/")), FILE_EXTENSIONS);
}
/**
* Constructor with given locations and extensions.
*/
public ResourceDocumentSource(List<Resource> locations, List<String> extensions) {
this.locations = new ArrayList<>(locations);
this.extensions = new ArrayList<>(extensions);
}
/**
* Return the configured locations where to check for documents.
*/
public List<Resource> getLocations() {
return this.locations;
}
/**
* Return the file extensions to try when checking for documents by name.
*/
public List<String> getExtensions() {
return this.extensions;
}
@Override
public Mono<String> getDocument(String name) {
return Flux.fromIterable(this.locations)
.flatMapIterable(location -> getCandidateResources(name, location))
.filter(Resource::exists)
.next()
.map(this::resourceToString)
.switchIfEmpty(Mono.fromRunnable(() -> {
throw new IllegalStateException(
"Failed to find document, name='" + name + "', under location(s)=" +
this.locations.stream().map(Resource::toString).collect(Collectors.toList()));
}))
.subscribeOn(Schedulers.boundedElastic());
}
private List<Resource> getCandidateResources(String name, Resource location) {
return this.extensions.stream()
.map(ext -> {
try {
return location.createRelative(name + ext);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
})
.collect(Collectors.toList());
}
private String resourceToString(Resource resource) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
FileCopyUtils.copy(resource.getInputStream(), out);
return new String(out.toByteArray(), StandardCharsets.UTF_8);
}
catch (IOException ex) {
throw new IllegalArgumentException(
"Found resource: " + resource.getDescription() + " but failed to read it", ex);
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2020-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.
*/
/**
* Support classes for Spring GraphQL.
*/
@NonNullApi
@NonNullFields
package org.springframework.graphql.support;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -34,6 +34,7 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.support.MapExecutionResult;
import org.springframework.graphql.web.webflux.GraphQlWebSocketMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.web.reactive.socket.CloseStatus;
@@ -64,9 +65,9 @@ public class MockWebSocketGraphQlTransportTests {
private final WebSocketGraphQlTransport transport =
WebSocketGraphQlTransport.builder(URI.create("/"), this.testClient).build();
private final ExecutionResult result1 = MapExecutionResult.forData(Collections.singletonMap("key1", "value1"));
private final ExecutionResult result1 = MapExecutionResult.forDataOnly(Collections.singletonMap("key1", "value1"));
private final ExecutionResult result2 = MapExecutionResult.forData(Collections.singletonMap("key2", "value2"));
private final ExecutionResult result2 = MapExecutionResult.forDataOnly(Collections.singletonMap("key2", "value2"));
@Test

View File

@@ -0,0 +1,83 @@
/*
* 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.support;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ResourceDocumentSource}.
* @author Rossen Stoyanchev
*/
public class CachingDocumentSourceTests {
private CachingDocumentSource source;
@BeforeEach
void setUp() {
DocumentSource resourceSource = new ResourceDocumentSource(
Collections.singletonList(new ClassPathResource("books/")),
ResourceDocumentSource.FILE_EXTENSIONS);
this.source = new CachingDocumentSource(resourceSource);
}
@Test
void cachingDefault() {
assertThat(this.source.isCacheEnabled()).isTrue();
}
@Test
void cachingOn() {
this.source.setCacheEnabled(true);
Mono<String> documentMono1 = source.getDocument("book-document");
Mono<String> documentMono2 = source.getDocument("book-document");
assertThat(documentMono1).isSameAs(documentMono2);
String document1 = documentMono1.block();
String document2 = documentMono2.block();
assertThat(document1).isSameAs(document2);
}
@Test
void cachingOff() {
this.source.setCacheEnabled(false);
Mono<String> documentMono1 = source.getDocument("book-document");
Mono<String> documentMono2 = source.getDocument("book-document");
assertThat(documentMono1).isNotSameAs(documentMono2);
String document1 = documentMono1.block();
String document2 = documentMono2.block();
assertThat(document1).isNotSameAs(document2);
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.support;
import java.time.Duration;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ResourceDocumentSource}.
* @author Rossen Stoyanchev
*/
public class ResourceDocumentSourceTests {
private ResourceDocumentSource source;
@BeforeEach
void setUp() {
this.source = new ResourceDocumentSource(
Collections.singletonList(new ClassPathResource("books/")),
ResourceDocumentSource.FILE_EXTENSIONS);
}
@Test
void getDocument() {
String content = this.source.getDocument("book-document").block(Duration.ofSeconds(5));
assertThat(content).startsWith("bookById(id:\"1\"");
}
@Test
void getDocumentNotFound() {
StepVerifier.create(this.source.getDocument("invalid"))
.expectErrorMessage(
"Failed to find document, name='invalid', " +
"under location(s)=[class path resource [books/]]")
.verify(Duration.ofSeconds(5));
}
}

View File

@@ -0,0 +1,4 @@
bookById(id:"1") {
id
name
}