Apply Spring JavaFormat to Spring GraphQL Boot starter

This commit applies the build conventions to the Spring Boot GraphQL
starter module and fixes all formatting issues.

See gh-54
This commit is contained in:
Brian Clozel
2021-06-01 20:56:41 +02:00
parent fc3a6403cb
commit f8b90b592d
29 changed files with 386 additions and 258 deletions

View File

@@ -4,6 +4,7 @@ plugins {
id 'org.springframework.boot' version '2.4.5' apply false
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java-library'
id "org.springframework.graphql.conventions"
}
description = "GraphQL Spring Boot Starter"

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot;
import java.util.stream.Collectors;
@@ -22,6 +23,7 @@ import graphql.execution.instrumentation.Instrumentation;
import graphql.schema.idl.RuntimeWiring;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -31,7 +33,14 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.GraphQlSource;
@Configuration
/**
* {@link EnableAutoConfiguration Auto-configuration} for creating a
* {@link GraphQlSource}.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(GraphQL.class)
@ConditionalOnMissingBean(GraphQlSource.class)
@EnableConfigurationProperties(GraphQlProperties.class)
@@ -42,31 +51,30 @@ public class GraphQlAutoConfiguration {
return builder.build();
}
@Configuration
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(GraphQlSource.Builder.class)
static class GraphQlSourceConfiguration {
public static class GraphQlSourceConfiguration {
@Bean
@ConditionalOnMissingBean
public RuntimeWiring runtimeWiring(ObjectProvider<RuntimeWiringCustomizer> customizers) {
RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring();
customizers.orderedStream().forEach(customizer -> customizer.customize(builder));
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
@Bean
public GraphQlSource.Builder graphQlSourceBuilder(
GraphQlProperties properties, RuntimeWiring runtimeWiring,
ObjectProvider<DataFetcherExceptionResolver> exceptionResolversProvider,
ResourceLoader resourceLoader, ObjectProvider<Instrumentation> instrumentationsProvider) {
public GraphQlSource.Builder graphQlSourceBuilder(GraphQlProperties properties, RuntimeWiring runtimeWiring,
ObjectProvider<DataFetcherExceptionResolver> exceptionResolversProvider, ResourceLoader resourceLoader,
ObjectProvider<Instrumentation> instrumentationsProvider) {
String schemaLocation = properties.getSchema().getLocation();
return GraphQlSource.builder()
.schemaResource(resourceLoader.getResource(schemaLocation))
return GraphQlSource.builder().schemaResource(resourceLoader.getResource(schemaLocation))
.runtimeWiring(runtimeWiring)
.exceptionResolvers(exceptionResolversProvider.orderedStream().collect(Collectors.toList()))
.instrumentation(instrumentationsProvider.orderedStream().collect(Collectors.toList()));
}
}
}

View File

@@ -13,12 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot;
import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* {@link ConfigurationProperties properties} for Spring GraphQL.
*
* @author Brian Clozel
* @since 1.0.0
*/
@ConfigurationProperties(prefix = "spring.graphql")
public class GraphQlProperties {
@@ -68,7 +75,6 @@ public class GraphQlProperties {
return this.printer;
}
public static class Printer {
/**
@@ -96,11 +102,11 @@ public class GraphQlProperties {
public void setPath(String path) {
this.path = path;
}
}
}
public static class WebSocket {
/**
@@ -109,7 +115,8 @@ public class GraphQlProperties {
private String path;
/**
* Time within which the initial {@code CONNECTION_INIT} type message must be received.
* Time within which the initial {@code CONNECTION_INIT} type message must be
* received.
*/
private Duration connectionInitTimeout = Duration.ofSeconds(60);
@@ -128,5 +135,7 @@ public class GraphQlProperties {
public void setConnectionInitTimeout(Duration connectionInitTimeout) {
this.connectionInitTimeout = connectionInitTimeout;
}
}
}

View File

@@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot;
import graphql.GraphQL;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
@@ -26,7 +28,14 @@ import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
@Configuration
/**
* {@link EnableAutoConfiguration Auto-configuration} for creating a
* {@link GraphQlService}.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(GraphQL.class)
@ConditionalOnMissingBean(GraphQlService.class)
@AutoConfigureAfter(GraphQlAutoConfiguration.class)

View File

@@ -13,13 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot;
import graphql.schema.idl.RuntimeWiring;
/**
* Callback interface that can be used to customize the GraphQL
* {@link RuntimeWiring.Builder}.
*
* @author Brian Clozel
* @since 1.0.0
*/
@FunctionalInterface
public interface RuntimeWiringCustomizer {
/**
* Callback to customize a {@link RuntimeWiring.Builder} instance.
* @param builder builder instance to customize
*/
void customize(RuntimeWiring.Builder builder);
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot;
import java.util.Collections;
@@ -25,6 +26,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -43,16 +45,21 @@ import org.springframework.graphql.web.webflux.GraphQlWebSocketHandler;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.function.server.RequestPredicates;
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 org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.server.support.WebSocketUpgradeHandlerPredicate;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
@Configuration
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
* WebFlux.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass(GraphQL.class)
@ConditionalOnBean(GraphQlSource.class)
@@ -61,13 +68,11 @@ public class WebFluxGraphQlAutoConfiguration {
private static final Log logger = LogFactory.getLog(WebFluxGraphQlAutoConfiguration.class);
@Bean
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(ObjectProvider<WebInterceptor> interceptors, GraphQlService service) {
return WebGraphQlHandler.builder(service)
.interceptors(interceptors.orderedStream().collect(Collectors.toList()))
.build();
.interceptors(interceptors.orderedStream().collect(Collectors.toList())).build();
}
@Bean
@@ -86,33 +91,33 @@ public class WebFluxGraphQlAutoConfiguration {
logger.info("GraphQL endpoint HTTP POST " + path);
}
RouterFunctions.Builder builder = RouterFunctions.route()
.GET(path, req -> ServerResponse.ok().bodyValue(resource))
.POST(path, accept(MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_JSON)), handler::handleRequest);
.GET(path, (req) -> ServerResponse.ok().bodyValue(resource))
.POST(path, RequestPredicates.accept(MediaType.APPLICATION_JSON)
.and(RequestPredicates.contentType(MediaType.APPLICATION_JSON)), handler::handleRequest);
if (properties.getSchema().getPrinter().isEnabled()) {
SchemaPrinter printer = new SchemaPrinter();
builder = builder.GET(path + properties.getSchema().getPrinter().getPath(),
req -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(printer.print(graphQlSource.schema())));
builder = builder.GET(path + properties.getSchema().getPrinter().getPath(), (req) -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN).bodyValue(printer.print(graphQlSource.schema())));
}
return builder.build();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.graphql.websocket", name = "path")
static class WebSocketConfiguration {
public static class WebSocketConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlWebSocketHandler graphQlWebSocketHandler(
WebGraphQlHandler webGraphQlHandler, GraphQlProperties properties, ServerCodecConfigurer configurer) {
public GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
GraphQlProperties properties, ServerCodecConfigurer configurer) {
return new GraphQlWebSocketHandler(
webGraphQlHandler, configurer, properties.getWebsocket().getConnectionInitTimeout());
return new GraphQlWebSocketHandler(webGraphQlHandler, configurer,
properties.getWebsocket().getConnectionInitTimeout());
}
@Bean
public HandlerMapping graphQlWebSocketEndpoint(
GraphQlWebSocketHandler graphQlWebSocketHandler, GraphQlProperties properties) {
public HandlerMapping graphQlWebSocketEndpoint(GraphQlWebSocketHandler graphQlWebSocketHandler,
GraphQlProperties properties) {
String path = properties.getWebsocket().getPath();
if (logger.isInfoEnabled()) {
@@ -124,6 +129,7 @@ public class WebFluxGraphQlAutoConfiguration {
mapping.setOrder(-2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean)
return mapping;
}
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot;
import java.util.Collections;
@@ -28,6 +29,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -48,6 +50,7 @@ import org.springframework.graphql.web.webmvc.GraphQlWebSocketHandler;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.function.RequestPredicates;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;
@@ -56,10 +59,14 @@ import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import org.springframework.web.socket.server.support.WebSocketHandlerMapping;
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
import static org.springframework.web.servlet.function.RequestPredicates.accept;
import static org.springframework.web.servlet.function.RequestPredicates.contentType;
@Configuration
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
* Spring MVC.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(GraphQL.class)
@ConditionalOnBean(GraphQlSource.class)
@@ -68,17 +75,14 @@ public class WebMvcGraphQlAutoConfiguration {
private static final Log logger = LogFactory.getLog(WebMvcGraphQlAutoConfiguration.class);
@Bean
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(
ObjectProvider<WebInterceptor> interceptorsProvider, GraphQlService service,
ObjectProvider<ThreadLocalAccessor> accessorsProvider) {
public WebGraphQlHandler webGraphQlHandler(ObjectProvider<WebInterceptor> interceptorsProvider,
GraphQlService service, ObjectProvider<ThreadLocalAccessor> accessorsProvider) {
return WebGraphQlHandler.builder(service)
.interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList()))
.threadLocalAccessors(accessorsProvider.orderedStream().collect(Collectors.toList()))
.build();
.threadLocalAccessors(accessorsProvider.orderedStream().collect(Collectors.toList())).build();
}
@Bean
@@ -96,36 +100,33 @@ public class WebMvcGraphQlAutoConfiguration {
if (logger.isInfoEnabled()) {
logger.info("GraphQL endpoint HTTP POST " + path);
}
RouterFunctions.Builder builder = RouterFunctions.route()
.GET(path, req -> ServerResponse.ok().body(resource))
.POST(path, contentType(MediaType.APPLICATION_JSON).and(accept(MediaType.APPLICATION_JSON)), handler::handleRequest);
RouterFunctions.Builder builder = RouterFunctions.route().GET(path, (req) -> ServerResponse.ok().body(resource))
.POST(path, RequestPredicates.contentType(MediaType.APPLICATION_JSON)
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::handleRequest);
if (properties.getSchema().getPrinter().isEnabled()) {
SchemaPrinter printer = new SchemaPrinter();
builder = builder.GET(path + properties.getSchema().getPrinter().getPath(),
req -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.body(printer.print(graphQlSource.schema())));
builder = builder.GET(path + properties.getSchema().getPrinter().getPath(), (req) -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN).body(printer.print(graphQlSource.schema())));
}
return builder.build();
}
@ConditionalOnClass({ServerContainer.class, WebSocketHandler.class})
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ ServerContainer.class, WebSocketHandler.class })
@ConditionalOnProperty(prefix = "spring.graphql.websocket", name = "path")
static class WebSocketConfiguration {
public static class WebSocketConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlWebSocketHandler graphQlWebSocketHandler(
WebGraphQlHandler webGraphQlHandler, GraphQlProperties properties, HttpMessageConverters converters) {
public GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
GraphQlProperties properties, HttpMessageConverters converters) {
HttpMessageConverter<?> converter = converters.getConverters().stream()
.filter(candidate -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON))
.findFirst()
.filter((candidate) -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON)).findFirst()
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
return new GraphQlWebSocketHandler(
webGraphQlHandler, converter, properties.getWebsocket().getConnectionInitTimeout());
return new GraphQlWebSocketHandler(webGraphQlHandler, converter,
properties.getWebsocket().getConnectionInitTimeout());
}
@Bean
@@ -144,6 +145,4 @@ public class WebMvcGraphQlAutoConfiguration {
}
}

View File

@@ -26,6 +26,12 @@ import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
/**
* Default implementation for {@link GraphQlTagsProvider}.
*
* @author Brian Clozel
* @since 1.0.0
*/
public class DefaultGraphQlTagsProvider implements GraphQlTagsProvider {
private final List<GraphQlTagsContributor> contributors;
@@ -34,9 +40,9 @@ public class DefaultGraphQlTagsProvider implements GraphQlTagsProvider {
this.contributors = contributors;
}
@Override
public Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result, Throwable exception) {
public Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
Throwable exception) {
Tags tags = Tags.of(GraphQlTags.executionOutcome(result, exception));
for (GraphQlTagsContributor contributor : this.contributors) {
tags = tags.and(contributor.getExecutionTags(parameters, result, exception));
@@ -54,11 +60,13 @@ public class DefaultGraphQlTagsProvider implements GraphQlTagsProvider {
}
@Override
public Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters, Throwable exception) {
public Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
Throwable exception) {
Tags tags = Tags.of(GraphQlTags.dataFetchingOutcome(exception), GraphQlTags.dataFetchingPath(parameters));
for (GraphQlTagsContributor contributor : this.contributors) {
tags = tags.and(contributor.getDataFetchingTags(dataFetcher, parameters, exception));
}
return tags;
}
}

View File

@@ -33,12 +33,15 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring GraphQL
* endpoints.
* {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring
* GraphQL endpoints.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter({MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class})
@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class })
@ConditionalOnBean(MeterRegistry.class)
@EnableConfigurationProperties(GraphQlMetricsProperties.class)
public class GraphQlMetricsAutoConfiguration {
@@ -54,4 +57,5 @@ public class GraphQlMetricsAutoConfiguration {
GraphQlTagsProvider tagsProvider, GraphQlMetricsProperties properties) {
return new GraphQlMetricsInstrumentation(meterRegistry, tagsProvider, properties.getAutotime());
}
}

View File

@@ -31,8 +31,9 @@ import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Timer;
import org.springframework.boot.actuate.metrics.AutoTimer;
import org.springframework.lang.Nullable;
public class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
private final MeterRegistry registry;
@@ -40,7 +41,7 @@ public class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
private final AutoTimer autoTimer;
public GraphQlMetricsInstrumentation(MeterRegistry registry, GraphQlTagsProvider tagsProvider, AutoTimer autoTimer) {
GraphQlMetricsInstrumentation(MeterRegistry registry, GraphQlTagsProvider tagsProvider, AutoTimer autoTimer) {
this.registry = registry;
this.tagsProvider = tagsProvider;
this.autoTimer = autoTimer;
@@ -59,12 +60,14 @@ public class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
return new SimpleInstrumentationContext<ExecutionResult>() {
@Override
public void onCompleted(ExecutionResult result, Throwable exc) {
Iterable<Tag> tags = tagsProvider.getExecutionTags(parameters, result, exc);
Iterable<Tag> tags = GraphQlMetricsInstrumentation.this.tagsProvider.getExecutionTags(parameters,
result, exc);
state.tags(tags).stopTimer();
if (!result.getErrors().isEmpty()) {
result.getErrors().forEach(error -> {
registry.counter("graphql.error", tagsProvider.getErrorTags(parameters, error)).increment();
});
result.getErrors()
.forEach((error) -> GraphQlMetricsInstrumentation.this.registry.counter("graphql.error",
GraphQlMetricsInstrumentation.this.tagsProvider.getErrorTags(parameters, error))
.increment());
}
}
};
@@ -73,7 +76,8 @@ public class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
}
@Override
public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters) {
public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher,
InstrumentationFieldFetchParameters parameters) {
if (this.autoTimer.isEnabled() && !parameters.isTrivialDataFetcher()) {
return (environment) -> {
Timer.Sample sample = Timer.start(this.registry);
@@ -81,9 +85,8 @@ public class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
Object value = dataFetcher.get(environment);
if (value instanceof CompletionStage<?>) {
CompletionStage<?> completion = (CompletionStage<?>) value;
return completion.whenComplete((result, error) -> {
recordDataFetcherMetric(sample, dataFetcher, parameters, error);
});
return completion.whenComplete(
(result, error) -> recordDataFetcherMetric(sample, dataFetcher, parameters, error));
}
else {
recordDataFetcherMetric(sample, dataFetcher, parameters, null);
@@ -100,13 +103,13 @@ public class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
return super.instrumentDataFetcher(dataFetcher, parameters);
}
private void recordDataFetcherMetric(Timer.Sample sample, DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters, Throwable throwable) {
private void recordDataFetcherMetric(Timer.Sample sample, DataFetcher<?> dataFetcher,
InstrumentationFieldFetchParameters parameters, @Nullable Throwable throwable) {
Timer.Builder timer = this.autoTimer.builder("graphql.datafetcher");
timer.tags(this.tagsProvider.getDataFetchingTags(dataFetcher, parameters, throwable));
sample.stop(timer.register(this.registry));
}
static class RequestMetricsInstrumentationState implements InstrumentationState {
private final MeterRegistry registry;
@@ -120,18 +123,19 @@ public class GraphQlMetricsInstrumentation extends SimpleInstrumentation {
this.registry = registry;
}
public RequestMetricsInstrumentationState tags(Iterable<Tag> tags) {
RequestMetricsInstrumentationState tags(Iterable<Tag> tags) {
this.timer.tags(tags);
return this;
}
public void startTimer() {
void startTimer() {
this.sample = Timer.start(this.registry);
}
public void stopTimer() {
void stopTimer() {
this.sample.stop(this.timer.register(this.registry));
}
}
}

View File

@@ -21,7 +21,13 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* This class could be merged with {@link org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties}
* {@link ConfigurationProperties properties} for Spring GraphQL.
* <p>
* This class could be later merged with
* {@link org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties}.
*
* @author Brian Clozel
* @since 1.0.0
*/
@ConfigurationProperties("management.metrics.graphql")
public class GraphQlMetricsProperties {
@@ -35,4 +41,5 @@ public class GraphQlMetricsProperties {
public AutoTimeProperties getAutotime() {
return this.autotime;
}
}

View File

@@ -27,12 +27,14 @@ import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchPar
import graphql.schema.GraphQLObjectType;
import io.micrometer.core.instrument.Tag;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
/**
* Factory methods for Tags associated with a GraphQL requests.
*
* Factory methods for Tags associated with a GraphQL request.
*
* @author Brian Clozel
* @since 1.0.0
*/
public final class GraphQlTags {
@@ -42,7 +44,11 @@ public final class GraphQlTags {
private static final Tag UNKNOWN_ERRORTYPE = Tag.of("errorType", "UNKNOWN");
public static Tag executionOutcome(ExecutionResult result, Throwable exception) {
private GraphQlTags() {
}
public static Tag executionOutcome(ExecutionResult result, @Nullable Throwable exception) {
if (exception == null && result.getErrors().isEmpty()) {
return OUTCOME_SUCCESS;
}
@@ -78,19 +84,19 @@ public final class GraphQlTags {
return Tag.of("errorPath", builder.toString());
}
public static Tag dataFetchingOutcome(Throwable exception) {
return (exception == null) ? OUTCOME_SUCCESS : OUTCOME_ERROR;
public static Tag dataFetchingOutcome(@Nullable Throwable exception) {
return (exception != null) ? OUTCOME_ERROR : OUTCOME_SUCCESS;
}
public static Tag dataFetchingPath(InstrumentationFieldFetchParameters parameters) {
ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo();
StringBuilder dataFetchingType = new StringBuilder();
if (executionStepInfo.hasParent() &&
executionStepInfo.getParent().getType() instanceof GraphQLObjectType) {
if (executionStepInfo.hasParent() && executionStepInfo.getParent().getType() instanceof GraphQLObjectType) {
dataFetchingType.append(((GraphQLObjectType) executionStepInfo.getParent().getType()).getName());
dataFetchingType.append('.');
}
dataFetchingType.append(executionStepInfo.getPath().getSegmentName());
return Tag.of("path", dataFetchingType.toString());
}
}

View File

@@ -23,11 +23,23 @@ import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchPar
import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.Tag;
import org.springframework.lang.Nullable;
/**
* A contributor of {@link Tag Tags} for Spring GraphQL-based request handling. Typically
* used by a {@link GraphQlTagsProvider} to provide tags in addition to its defaults.
*
* @author Brian Clozel
* @since 1.0.0
*/
public interface GraphQlTagsContributor {
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result, Throwable exception);
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
@Nullable Throwable exception);
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters, Throwable exception);
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
@Nullable Throwable exception);
}

View File

@@ -23,12 +23,22 @@ import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchPar
import graphql.schema.DataFetcher;
import io.micrometer.core.instrument.Tag;
import org.springframework.lang.Nullable;
/**
* Provides {@link Tag Tags} for Spring GraphQL-based request handling.
*
* @author Brian Clozel
* @since 1.0.0
*/
public interface GraphQlTagsProvider {
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result, Throwable exception);
Iterable<Tag> getExecutionTags(InstrumentationExecutionParameters parameters, ExecutionResult result,
@Nullable Throwable exception);
Iterable<Tag> getErrorTags(InstrumentationExecutionParameters parameters, GraphQLError error);
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters, Throwable exception);
Iterable<Tag> getDataFetchingTags(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters,
@Nullable Throwable exception);
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2020-2021 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.
*/
/**
* Provides instrumentation support.
*/

View File

@@ -1,8 +1,23 @@
/*
* Copyright 2020-2021 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.
*/
/**
* Auto-configuration classes to configure
* {@link org.springframework.graphql.execution.GraphQlSource},
* {@link org.springframework.graphql.GraphQlService}, and HTTP and WebSocket
* endpoints.
* {@link org.springframework.graphql.GraphQlService}, and HTTP and WebSocket endpoints.
*/
@NonNullApi
@NonNullFields

View File

@@ -31,8 +31,9 @@ import org.springframework.graphql.test.tester.GraphQlTester;
*
* @author Brian Clozel
* @see GraphQlTesterAutoConfiguration
* @since 1.0.0
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited

View File

@@ -30,18 +30,19 @@ import org.springframework.web.reactive.function.client.WebClient;
/**
* Auto-configuration for {@link GraphQlTester} in mock environments.
*
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({WebClient.class, WebTestClient.class, GraphQlTester.class})
@ConditionalOnClass({ WebClient.class, WebTestClient.class, GraphQlTester.class })
@AutoConfigureAfter(value = WebTestClientMockMvcAutoConfiguration.class,
name = "org.springframework.boot.test.autoconfigure.web.reactive.WebTestClientAutoConfiguration")
public class GraphQlTesterAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(WebTestClient.class)
static class WebTestClientGraphQlTesterConfiguration {
public static class WebTestClientGraphQlTesterConfiguration {
@Bean
public GraphQlTester clientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) {
@@ -54,7 +55,7 @@ public class GraphQlTesterAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebTestClient.class)
@ConditionalOnBean(WebGraphQlHandler.class)
static class WebGraphQlHandlerGraphQlTesterConfiguration {
public static class WebGraphQlHandlerGraphQlTesterConfiguration {
@Bean
public GraphQlTester handlerGraphQlTester(WebGraphQlHandler handler) {

View File

@@ -42,7 +42,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.StringUtils;
/**
* {@link ContextCustomizer} for {@link GraphQlTester}
* {@link ContextCustomizer} for {@link GraphQlTester}.
*
* @author Brian Clozel
*/
@@ -65,12 +65,13 @@ class GraphQlTesterContextCustomizer implements ContextCustomizer {
}
private void registerGraphQlTester(BeanDefinitionRegistry registry) {
RootBeanDefinition definition = new RootBeanDefinition(GraphQlTesterContextCustomizer.GraphQlTesterRegistrar.class);
RootBeanDefinition definition = new RootBeanDefinition(
GraphQlTesterContextCustomizer.GraphQlTesterRegistrar.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(GraphQlTesterContextCustomizer.GraphQlTesterRegistrar.class.getName(), definition);
registry.registerBeanDefinition(GraphQlTesterContextCustomizer.GraphQlTesterRegistrar.class.getName(),
definition);
}
@Override
public boolean equals(Object obj) {
return (obj != null) && (obj.getClass() == getClass());
@@ -81,8 +82,8 @@ class GraphQlTesterContextCustomizer implements ContextCustomizer {
return getClass().hashCode();
}
private static class GraphQlTesterRegistrar implements BeanDefinitionRegistryPostProcessor, Ordered, BeanFactoryAware {
private static class GraphQlTesterRegistrar
implements BeanDefinitionRegistryPostProcessor, Ordered, BeanFactoryAware {
private BeanFactory beanFactory;
@@ -107,8 +108,9 @@ class GraphQlTesterContextCustomizer implements ContextCustomizer {
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE -1;
return Ordered.LOWEST_PRECEDENCE - 1;
}
}
public static class GraphQlTesterFactory implements FactoryBean<GraphQlTester>, ApplicationContextAware {
@@ -170,4 +172,5 @@ class GraphQlTesterContextCustomizer implements ContextCustomizer {
}
}
}

View File

@@ -40,7 +40,8 @@ class GraphQlTesterContextCustomizerFactory implements ContextCustomizerFactory
@Override
public ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(testClass, SpringBootTest.class);
SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(testClass,
SpringBootTest.class);
return (springBootTest != null && isGraphQlTesterPresent()) ? new GraphQlTesterContextCustomizer() : null;
}
@@ -48,4 +49,5 @@ class GraphQlTesterContextCustomizerFactory implements ContextCustomizerFactory
return ClassUtils.isPresent(WEBTESTCLIENT_CLASS, getClass().getClassLoader())
&& ClassUtils.isPresent(GRAPHQLTESTER_CLASS, getClass().getClassLoader());
}
}

View File

@@ -32,13 +32,16 @@ import org.springframework.web.reactive.function.client.WebClient;
/**
* Auto-configuration for {@link WebTestClient} support with {@link MockMvc}.
* <p>Temporary workaround for upcoming enhancement request in Spring Boot 2.6.0.
* <p>
* Temporary workaround for upcoming enhancement request in Spring Boot 2.6.0.
*
* @author Brian Clozel
* @see <a href="https://github.com/spring-projects/spring-boot/issues/23067">Spring Boot 2.6.x issue</a>
* @since 1.0.0
* @see <a href="https://github.com/spring-projects/spring-boot/issues/23067">Spring Boot
* 2.6.x issue</a>
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ WebClient.class, WebTestClient.class, MockMvcWebTestClient.class})
@ConditionalOnClass({ WebClient.class, WebTestClient.class, MockMvcWebTestClient.class })
@AutoConfigureAfter(name = "org.springframework.boot.test.autoconfigure.web.reactive.WebTestClientAutoConfiguration")
public class WebTestClientMockMvcAutoConfiguration {
@@ -52,5 +55,5 @@ public class WebTestClientMockMvcAutoConfiguration {
}
return builder.build();
}
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2020-2021 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.boot;
public class Book {
@@ -21,7 +37,7 @@ public class Book {
}
public String getId() {
return id;
return this.id;
}
public void setId(String id) {
@@ -29,7 +45,7 @@ public class Book {
}
public String getName() {
return name;
return this.name;
}
public void setName(String name) {
@@ -37,7 +53,7 @@ public class Book {
}
public int getPageCount() {
return pageCount;
return this.pageCount;
}
public void setPageCount(int pageCount) {
@@ -45,10 +61,11 @@ public class Book {
}
public String getAuthor() {
return author;
return this.author;
}
public void setAuthor(String author) {
this.author = author;
}
}

View File

@@ -35,10 +35,9 @@ class GraphQlAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GraphQlAutoConfiguration.class));
@Test
void shouldFailWhenSchemaFileIsMissing() {
contextRunner.run((context) -> {
this.contextRunner.run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure().getRootCause().hasMessage("'schemaResource' does not exist");
});
@@ -46,29 +45,27 @@ class GraphQlAutoConfigurationTests {
@Test
void shouldCreateBuilderWithSdl() {
contextRunner
.withPropertyValues("spring.graphql.schema.location:classpath:books/schema.graphqls")
this.contextRunner.withPropertyValues("spring.graphql.schema.location:classpath:books/schema.graphqls")
.run((context) -> assertThat(context).hasSingleBean(GraphQlSource.class));
}
@Test
void shouldUseProgrammaticallyDefinedBuilder() {
contextRunner
.withPropertyValues("spring.graphql.schema.location:classpath:books/schema.graphqls")
.withUserConfiguration(CustomGraphQlBuilderConfiguration.class)
.run((context) -> {
this.contextRunner.withPropertyValues("spring.graphql.schema.location:classpath:books/schema.graphqls")
.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
assertThat(context).hasBean("customGraphQlSourceBuilder");
assertThat(context).hasSingleBean(GraphQlSource.Builder.class);
});
}
@Configuration
@Configuration(proxyBeanMethods = false)
static class CustomGraphQlBuilderConfiguration {
@Bean
public GraphQlSource.Builder customGraphQlSourceBuilder() {
GraphQlSource.Builder customGraphQlSourceBuilder() {
return GraphQlSource.builder().schemaResource(new ClassPathResource("books/schema.graphqls"));
}
}
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2020-2021 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.boot;
import java.util.Arrays;
@@ -6,25 +22,24 @@ import java.util.List;
import graphql.schema.DataFetcher;
import reactor.core.publisher.Flux;
public class GraphQlDataFetchers {
public final class GraphQlDataFetchers {
private static List<Book> books = Arrays.asList(
new Book("book-1", "GraphQL for beginners", 100, "John GraphQL"),
private static List<Book> books = Arrays.asList(new Book("book-1", "GraphQL for beginners", 100, "John GraphQL"),
new Book("book-2", "Harry Potter and the Philosopher's Stone", 223, "Joanne Rowling"),
new Book("book-3", "Moby Dick", 635, "Moby Dick"),
new Book("book-3", "Moby Dick", 635, "Moby Dick"));
new Book("book-3", "Moby Dick", 635, "Moby Dick"), new Book("book-3", "Moby Dick", 635, "Moby Dick"));
private GraphQlDataFetchers() {
}
public static DataFetcher getBookByIdDataFetcher() {
return environment -> books.stream()
.filter(book -> book.getId().equals(environment.getArgument("id")))
.findFirst()
.orElse(null);
return (environment) -> books.stream().filter((book) -> book.getId().equals(environment.getArgument("id")))
.findFirst().orElse(null);
}
public static DataFetcher getBooksOnSale() {
return environment -> Flux.fromIterable(books)
.filter(book -> book.getPageCount() >= (int) environment.getArgument("minPages"));
return (environment) -> Flux.fromIterable(books)
.filter((book) -> book.getPageCount() >= (int) environment.getArgument("minPages"));
}
}

View File

@@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot;
import java.util.Collections;
import java.util.function.Consumer;
import graphql.schema.idl.TypeRuntimeWiring;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
@@ -35,124 +37,94 @@ import org.springframework.graphql.web.WebInterceptor;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;
class WebFluxApplicationContextTests {
private static final AutoConfigurations AUTO_CONFIGURATIONS = AutoConfigurations.of(
HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
CodecsAutoConfiguration.class, JacksonAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlServiceAutoConfiguration.class,
HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class, CodecsAutoConfiguration.class,
JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class, GraphQlServiceAutoConfiguration.class,
WebFluxGraphQlAutoConfiguration.class);
private static final String BASE_URL = "https://spring.example.org/graphql";
@Test
void query() {
testWithWebClient(client -> {
String query = "{" +
" bookById(id: \\\"book-1\\\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
testWithWebClient((client) -> {
String query = "{" + " bookById(id: \\\"book-1\\\"){ " + " id" + " name" + " pageCount"
+ " author" + " }" + "}";
client.post().uri("")
.bodyValue("{ \"query\": \"" + query + "\"}")
.exchange()
.expectStatus().isOk()
client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk()
.expectBody().jsonPath("data.bookById.name").isEqualTo("GraphQL for beginners");
});
}
@Test
void queryMissing() {
testWithWebClient(client -> client.post().uri("").bodyValue("{}").exchange().expectStatus().isBadRequest());
testWithWebClient((client) -> client.post().uri("").bodyValue("{}").exchange().expectStatus().isBadRequest());
}
@Test
void queryIsInvalidJson() {
testWithWebClient(client -> client.post().uri("").bodyValue(":)").exchange().expectStatus().isBadRequest());
testWithWebClient((client) -> client.post().uri("").bodyValue(":)").exchange().expectStatus().isBadRequest());
}
@Test
void interceptedQuery() {
testWithWebClient(client -> {
String query = "{" +
" bookById(id: \\\"book-1\\\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
testWithWebClient((client) -> {
String query = "{" + " bookById(id: \\\"book-1\\\"){ " + " id" + " name" + " pageCount"
+ " author" + " }" + "}";
client.post().uri("")
.bodyValue("{ \"query\": \"" + query + "\"}")
.exchange()
.expectStatus().isOk()
client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk()
.expectHeader().valueEquals("X-Custom-Header", "42");
});
}
@Test
void schemaEndpoint() {
testWithWebClient(client -> {
client.get().uri("/schema").accept(MediaType.ALL).exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.TEXT_PLAIN)
.expectBody(String.class).value(Matchers.containsString("type Book"));
});
testWithWebClient((client) -> client.get().uri("/schema").accept(MediaType.ALL).exchange().expectStatus().isOk()
.expectHeader().contentType(MediaType.TEXT_PLAIN).expectBody(String.class)
.value(Matchers.containsString("type Book")));
}
private void testWithWebClient(Consumer<WebTestClient> consumer) {
testWithApplicationContext(context -> {
WebTestClient client = WebTestClient.bindToApplicationContext(context)
.configureClient()
.defaultHeaders(headers -> {
testWithApplicationContext((context) -> {
WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient()
.defaultHeaders((headers) -> {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
})
.baseUrl(BASE_URL)
.build();
}).baseUrl(BASE_URL).build();
consumer.accept(client);
});
}
private void testWithApplicationContext(ContextConsumer<ApplicationContext> consumer) {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AUTO_CONFIGURATIONS)
new ReactiveWebApplicationContextRunner().withConfiguration(AUTO_CONFIGURATIONS)
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues(
"spring.main.web-application-type=reactive",
.withPropertyValues("spring.main.web-application-type=reactive",
"spring.graphql.schema.printer.enabled=true",
"spring.graphql.schema.location=classpath:books/schema.graphqls")
.run(consumer);
}
@Configuration(proxyBeanMethods = false)
static class DataFetchersConfiguration {
public static class DataFetchersConfiguration {
@Bean
public RuntimeWiringCustomizer bookDataFetcher() {
return (runtimeWiring) ->
runtimeWiring.type(newTypeWiring("Query")
.dataFetcher("bookById", GraphQlDataFetchers.getBookByIdDataFetcher()));
return (runtimeWiring) -> runtimeWiring.type(TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("bookById", GraphQlDataFetchers.getBookByIdDataFetcher()));
}
}
@Configuration(proxyBeanMethods = false)
static class CustomWebInterceptor {
public static class CustomWebInterceptor {
@Bean
public WebInterceptor customWebInterceptor() {
return (input, next) -> next.handle(input).map(output ->
output.transform(builder -> builder.responseHeader("X-Custom-Header", "42")));
return (input, next) -> next.handle(input)
.map((output) -> output.transform((builder) -> builder.responseHeader("X-Custom-Header", "42")));
}
}
}

View File

@@ -16,7 +16,7 @@
package org.springframework.graphql.boot;
import graphql.schema.idl.TypeRuntimeWiring;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
@@ -35,7 +35,6 @@ import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -54,18 +53,12 @@ class WebMvcApplicationContextTests {
@Test
void endpointHandlesGraphQlQuery() {
testWith(mockMvc -> {
String query = "{" +
" bookById(id: \\\"book-1\\\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
MvcResult asyncResult = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn();
mockMvc.perform(asyncDispatch(asyncResult))
.andExpect(status().isOk())
testWith((mockMvc) -> {
String query = "{" + " bookById(id: \\\"book-1\\\"){ " + " id" + " name" + " pageCount"
+ " author" + " }" + "}";
MvcResult asyncResult = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}"))
.andReturn();
mockMvc.perform(asyncDispatch(asyncResult)).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners"));
});
@@ -73,83 +66,73 @@ class WebMvcApplicationContextTests {
@Test
void missingQuery() {
testWith(mockMvc -> mockMvc.perform(post("/graphql").content("{}")).andExpect(status().isBadRequest()));
testWith((mockMvc) -> mockMvc.perform(post("/graphql").content("{}")).andExpect(status().isBadRequest()));
}
@Test
void invalidJson() {
testWith(mockMvc -> mockMvc.perform(post("/graphql").content(":)")).andExpect(status().isBadRequest()));
testWith((mockMvc) -> mockMvc.perform(post("/graphql").content(":)")).andExpect(status().isBadRequest()));
}
@Test
void interceptedQuery() {
testWith(mockMvc -> {
String query = "{" +
" bookById(id: \\\"book-1\\\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
MvcResult asyncResult = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn();
mockMvc.perform(asyncDispatch(asyncResult))
.andExpect(status().isOk())
testWith((mockMvc) -> {
String query = "{" + " bookById(id: \\\"book-1\\\"){ " + " id" + " name" + " pageCount"
+ " author" + " }" + "}";
MvcResult asyncResult = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}"))
.andReturn();
mockMvc.perform(asyncDispatch(asyncResult)).andExpect(status().isOk())
.andExpect(header().string("X-Custom-Header", "42"));
});
}
@Test
void schemaEndpoint() {
testWith(mockMvc -> {
mockMvc.perform(get("/graphql/schema")).andExpect(status().isOk())
.andExpect(content().contentType(MediaType.TEXT_PLAIN))
.andExpect(content().string(Matchers.containsString("type Book")));
});
testWith((mockMvc) -> mockMvc.perform(get("/graphql/schema")).andExpect(status().isOk())
.andExpect(content().contentType(MediaType.TEXT_PLAIN))
.andExpect(content().string(Matchers.containsString("type Book"))));
}
private void testWith(MockMvcConsumer mockMvcConsumer) {
new WebApplicationContextRunner()
.withConfiguration(AUTO_CONFIGURATIONS)
new WebApplicationContextRunner().withConfiguration(AUTO_CONFIGURATIONS)
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues(
"spring.main.web-application-type=servlet",
.withPropertyValues("spring.main.web-application-type=servlet",
"spring.graphql.schema.printer.enabled=true",
"spring.graphql.schema.location=classpath:books/schema.graphqls")
.run((context) -> {
MockHttpServletRequestBuilder builder = post("/graphql")
.contentType(MediaType.APPLICATION_JSON)
MockHttpServletRequestBuilder builder = post("/graphql").contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON);
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).defaultRequest(builder).build();
mockMvcConsumer.accept(mockMvc);
});
}
private static interface MockMvcConsumer {
private interface MockMvcConsumer {
void accept(MockMvc mockMvc) throws Exception;
}
@Configuration(proxyBeanMethods = false)
static class DataFetchersConfiguration {
public static class DataFetchersConfiguration {
@Bean
public RuntimeWiringCustomizer bookDataFetcher() {
return (builder) -> builder.type(newTypeWiring("Query")
.dataFetcher("bookById", GraphQlDataFetchers.getBookByIdDataFetcher()));
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
GraphQlDataFetchers.getBookByIdDataFetcher()));
}
}
@Configuration(proxyBeanMethods = false)
static class CustomWebInterceptor {
public static class CustomWebInterceptor {
@Bean
public WebInterceptor customWebInterceptor() {
return (input, next) -> next.handle(input).map(output ->
output.transform(builder -> builder.responseHeader("X-Custom-Header", "42")));
return (input, next) -> next.handle(input)
.map((output) -> output.transform((builder) -> builder.responseHeader("X-Custom-Header", "42")));
}
}
}

View File

@@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GraphQlTags}
*
*
* @author Brian Clozel
*/
class GraphQlTagsTests {
@@ -44,7 +44,8 @@ class GraphQlTagsTests {
@Test
void executionOutcomeShouldErrorWhenExceptionThrown() {
Tag outcomeTag = GraphQlTags.executionOutcome(new TestExecutionResult(), new IllegalArgumentException("test error"));
Tag outcomeTag = GraphQlTags.executionOutcome(new TestExecutionResult(),
new IllegalArgumentException("test error"));
assertThat(outcomeTag.getValue()).isEqualTo("ERROR");
}
@@ -58,21 +59,24 @@ class GraphQlTagsTests {
@Test
void errorTypeShouldBeDefinedIfPresent() {
GraphQLError error = GraphqlErrorBuilder.newError().errorType(ErrorType.DataFetchingException).message("test error").build();
GraphQLError error = GraphqlErrorBuilder.newError().errorType(ErrorType.DataFetchingException)
.message("test error").build();
Tag errorTypeTag = GraphQlTags.errorType(error);
assertThat(errorTypeTag.getValue()).isEqualTo("DataFetchingException");
}
@Test
void errorPathShouldUseJsonPathFormat() {
GraphQLError error = GraphqlErrorBuilder.newError().path(Arrays.asList("project", "name")).message("test error").build();
GraphQLError error = GraphqlErrorBuilder.newError().path(Arrays.asList("project", "name")).message("test error")
.build();
Tag errorPathTag = GraphQlTags.errorPath(error);
assertThat(errorPathTag.getValue()).isEqualTo("$.project.name");
}
@Test
void errorPathShouldUseJsonPathFormatForIndices() {
GraphQLError error = GraphqlErrorBuilder.newError().path(Arrays.asList("issues", "42", "title")).message("test error").build();
GraphQLError error = GraphqlErrorBuilder.newError().path(Arrays.asList("issues", "42", "title"))
.message("test error").build();
Tag errorPathTag = GraphQlTags.errorPath(error);
assertThat(errorPathTag.getValue()).isEqualTo("$.issues[*].title");
}
@@ -89,4 +93,4 @@ class GraphQlTagsTests {
assertThat(fetchingOutcomeTag.getValue()).isEqualTo("ERROR");
}
}
}

View File

@@ -75,7 +75,6 @@ class GraphQlTesterAutoConfigurationTests {
return mock(WebGraphQlHandler.class);
}
}
@Configuration(proxyBeanMethods = false)
@@ -84,7 +83,7 @@ class GraphQlTesterAutoConfigurationTests {
@Bean
WebTestClient webTestClient() {
RouterFunction<ServerResponse> routerFunction = RouterFunctions.route()
.POST("/graphql", request -> ServerResponse.ok().build()).build();
.POST("/graphql", (request) -> ServerResponse.ok().build()).build();
return WebTestClient.bindToRouterFunction(routerFunction).build();
}
@@ -94,4 +93,5 @@ class GraphQlTesterAutoConfigurationTests {
}
}
}

View File

@@ -43,11 +43,13 @@ import org.springframework.test.annotation.DirtiesContext;
*
* @author Brian Clozel
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "spring.main.web-application-type=reactive")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "spring.main.web-application-type=reactive")
@DirtiesContext
class GraphQlTesterContextCustomizerIntegrationTests {
@Autowired GraphQlTester graphQlTester;
@Autowired
GraphQlTester graphQlTester;
@Test
void test() {
@@ -69,6 +71,7 @@ class GraphQlTesterContextCustomizerIntegrationTests {
Map<String, HttpHandler> handlersMap = Collections.singletonMap(properties.getPath(), httpHandler);
return new ContextPathCompositeHandler(handlersMap);
}
}
static class TestHandler implements HttpHandler {
@@ -83,4 +86,5 @@ class GraphQlTesterContextCustomizerIntegrationTests {
}
}
}
}