Skip to content

Commit

Permalink
Add WebInterceptorExecution
Browse files Browse the repository at this point in the history
Arguably a better way to encapsulate support for WebInterceptor vs a
base class, more testable, and reduced public API surface.
  • Loading branch information
rstoyanchev committed Sep 18, 2020
1 parent 40a79ab commit 1f1ce72
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,21 @@
* GraphQL handler to expose as a WebFlux.fn endpoint via
* {@link org.springframework.web.reactive.function.server.RouterFunctions}.
*/
public class WebFluxGraphQLHandler extends WebHandlerSupport implements HandlerFunction<ServerResponse> {
public class WebFluxGraphQLHandler implements HandlerFunction<ServerResponse> {

private final WebInterceptorExecution executionChain;


public WebFluxGraphQLHandler(GraphQL graphQL, List<WebInterceptor> interceptors) {
super(graphQL, interceptors);
this.executionChain = new WebInterceptorExecution(graphQL, interceptors);
}


public Mono<ServerResponse> handle(ServerRequest request) {
return request.bodyToMono(WebInput.MAP_PARAMETERIZED_TYPE_REF)
.flatMap(body -> {
WebInput webInput = new WebInput(request.uri(), request.headers().asHttpHeaders(), body);
return executeQuery(webInput);
return this.executionChain.execute(webInput);
})
.flatMap(output -> ServerResponse.ok().bodyValue(output.toSpecification()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
import org.springframework.web.util.UriComponentsBuilder;

/**
* Represents the input to a GraphQL HTTP endpoint including URI, headers, and
* the query, operationName, and variables from the request body.
* Container for input from an HTTP request to a GraphQL endpoint, including
* URI, headers, and other inputs extracted from the body of the request.
*/
public class WebInput {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
import java.util.function.Consumer;

import graphql.ExecutionInput;
import graphql.GraphQL;
import reactor.core.publisher.Mono;

/**
* Allows interception of GraphQL over HTTP requests with possible customization
* of the input and the result of query execution.
* Interceptor for GraphQL over HTTP requests that allows customization of the
* {@link ExecutionInput} and the {@link graphql.ExecutionResult} of
* {@link GraphQL} query execution.
*/
public interface WebInterceptor {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import graphql.ExecutionInput;
import graphql.ExecutionResult;
Expand All @@ -27,49 +28,43 @@
import org.springframework.util.CollectionUtils;

/**
* Base class for GraphQL over HTTP handlers.
* Supports the use of {@link WebInterceptor}s to customize the
* {@link ExecutionInput} and the {@link ExecutionResult} of {@link GraphQL}
* query execution.
*/
public abstract class WebHandlerSupport {
class WebInterceptorExecution {

private final GraphQL graphQL;

private final List<WebInterceptor> interceptors;


public WebHandlerSupport(GraphQL graphQL, List<WebInterceptor> interceptors) {
WebInterceptorExecution(GraphQL graphQL, List<WebInterceptor> interceptors) {
this.graphQL = graphQL;
this.interceptors = (!CollectionUtils.isEmpty(interceptors) ?
Collections.unmodifiableList(new ArrayList<>(interceptors)) : Collections.emptyList());
}


public GraphQL getGraphQL() {
return this.graphQL;
}

public List<WebInterceptor> getInterceptors() {
return this.interceptors;
}


protected Mono<WebOutput> executeQuery(WebInput webInput) {
public Mono<WebOutput> execute(WebInput webInput) {
return createInputChain(webInput).flatMap(executionInput -> {
Mono<ExecutionResult> resultMono = Mono.fromFuture(getGraphQL().executeAsync(executionInput));
return createOutputChain(resultMono);
CompletableFuture<ExecutionResult> future = this.graphQL.executeAsync(executionInput);
return createOutputChain(Mono.fromFuture(future));
});
}

protected Mono<ExecutionInput> createInputChain(WebInput webInput) {
private Mono<ExecutionInput> createInputChain(WebInput webInput) {
Mono<ExecutionInput> preHandleMono = Mono.just(webInput.toExecutionInput());
for (WebInterceptor interceptor : this.interceptors) {
preHandleMono = preHandleMono.flatMap(input -> interceptor.preHandle(input, webInput));
}
return preHandleMono;
}

protected Mono<WebOutput> createOutputChain(Mono<ExecutionResult> resultMono) {
private Mono<WebOutput> createOutputChain(Mono<ExecutionResult> resultMono) {
Mono<WebOutput> outputMono = resultMono.map(WebOutput::new);
for (WebInterceptor interceptor : this.interceptors) {
for (int i = this.interceptors.size() - 1 ; i >= 0; i--) {
WebInterceptor interceptor = this.interceptors.get(i);
outputMono = outputMono.flatMap(interceptor::postHandle);
}
return outputMono;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@
* GraphQL handler to expose as a WebMvc.fn endpoint via
* {@link org.springframework.web.servlet.function.RouterFunctions}.
*/
public class WebMvcGraphQLHandler extends WebHandlerSupport implements HandlerFunction<ServerResponse> {
public class WebMvcGraphQLHandler implements HandlerFunction<ServerResponse> {

private final WebInterceptorExecution executionChain;


public WebMvcGraphQLHandler(GraphQL graphQL, List<WebInterceptor> interceptors) {
super(graphQL, interceptors);
this.executionChain = new WebInterceptorExecution(graphQL, interceptors);
}


Expand All @@ -51,7 +53,7 @@ public WebMvcGraphQLHandler(GraphQL graphQL, List<WebInterceptor> interceptors)
*/
public ServerResponse handle(ServerRequest request) throws ServletException {
WebInput webInput = new WebInput(request.uri(), request.headers().asHttpHeaders(), readBody(request));
Mono<WebOutput> outputMono = executeQuery(webInput);
Mono<WebOutput> outputMono = this.executionChain.execute(webInput);
return ServerResponse.ok().body(outputMono.map(ExecutionResult::toSpecification));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@


/**
* Simple wrapper around a GraphQL {@link ExecutionResult} that allows
* {@link #transform(Consumer) transformation} via a {@link Builder Builder}.
* {@link ExecutionResult} that wraps another in order to provide a convenient
* way to {@link #transform(Consumer) transform} it.
*/
public class WebOutput implements ExecutionResult {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.GraphQLDataFetchers;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.GraphQLDataFetchers;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
Expand Down Expand Up @@ -84,7 +85,7 @@ static class DataFetchersConfiguration {

@Bean
public RuntimeWiringCustomizer bookDataFetcher() {
return (runtimeWiring) -> runtimeWiring.type(newTypeWiring("Query")
return (builder) -> builder.type(newTypeWiring("Query")
.dataFetcher("bookById", GraphQLDataFetchers.getBookByIdDataFetcher()));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.springframework.boot.graphql;
package org.springframework.graphql;

public class Book {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.springframework.boot.graphql;
package org.springframework.graphql;

import java.util.Arrays;
import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2020-2020 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;

import java.io.File;
import java.net.URI;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.ExecutionInput;
import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;

import org.springframework.http.HttpHeaders;
import org.springframework.util.ResourceUtils;

import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

/**
* Unit tests for {@link WebInterceptorExecution}.
*/
public class WebInterceptorExecutionTests {

@Test
void testInterceptorInvocation() throws Exception {

StringBuilder sb = new StringBuilder();
List<WebInterceptor> interceptors = Arrays.asList(
new TestWebInterceptor(sb, 1), new TestWebInterceptor(sb, 2), new TestWebInterceptor(sb, 3));

String query = "{" +
" bookById(id: \\\"book-1\\\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";

ObjectMapper mapper = new ObjectMapper();
Map body = mapper.reader().readValue("{\"query\": \"" + query + "\"}", Map.class);
WebInput webInput = new WebInput(URI.create("/graphql"), new HttpHeaders(), body);

WebOutput webOutput = new WebInterceptorExecution(createGraphQL(), interceptors)
.execute(webInput).block();

assertEquals(":pre1:pre2:pre3:post3:post2:post1", sb.toString());
assertTrue(webOutput.isDataPresent());
}

private static GraphQL createGraphQL() throws Exception {
RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query").dataFetcher("bookById", GraphQLDataFetchers.getBookByIdDataFetcher()))
.build();

File file = ResourceUtils.getFile("classpath:books/schema.graphqls");
TypeDefinitionRegistry registry = new SchemaParser().parse(file);
SchemaGenerator generator = new SchemaGenerator();
GraphQLSchema schema = generator.makeExecutableSchema(registry, runtimeWiring);

return GraphQL.newGraphQL(schema).build();
}


private static class TestWebInterceptor implements WebInterceptor {

private final StringBuilder output;

private final int index;

public TestWebInterceptor(StringBuilder output, int index) {
this.output = output;
this.index = index;
}

@Override
public Mono<ExecutionInput> preHandle(ExecutionInput executionInput, WebInput webInput) {
this.output.append(":pre").append(this.index);
return Mono.delay(Duration.ofMillis(50)).map(aLong -> executionInput);
}

@Override
public Mono<WebOutput> postHandle(WebOutput webOutput) {
this.output.append(":post").append(this.index);
return Mono.delay(Duration.ofMillis(50)).map(aLong -> webOutput);
}
}

}

0 comments on commit 1f1ce72

Please sign in to comment.