WebOutput minor updates

Rename "headers" to "responseHeaders" and clarify those work for HTTP
request queries but not for queries over a WebSocket session.

Update getter for response headers to return a read-only wrapper.

Provide only one public constructor without headers.

See gh-42
This commit is contained in:
Rossen Stoyanchev
2021-04-09 12:50:42 +01:00
parent ddf107f8b1
commit b35e5f8c2f
8 changed files with 63 additions and 34 deletions

View File

@@ -141,8 +141,8 @@ class WebFluxApplicationContextTests {
public WebInterceptor customWebInterceptor() {
return new WebInterceptor() {
@Override
public Mono<WebOutput> postHandle(WebOutput webOutput) {
return Mono.just(webOutput.transform(output -> output.header("X-Custom-Header", "42")));
public Mono<WebOutput> postHandle(WebOutput output) {
return Mono.just(output.transform(builder -> builder.responseHeader("X-Custom-Header", "42")));
}
};
}

View File

@@ -137,8 +137,9 @@ class WebMvcApplicationContextTests {
public WebInterceptor customWebInterceptor() {
return new WebInterceptor() {
@Override
public Mono<WebOutput> postHandle(WebOutput webOutput) {
return Mono.just(webOutput.transform(output -> output.header("X-Custom-Header", "42")));
public Mono<WebOutput> postHandle(WebOutput output) {
return Mono.just(output.transform(builder ->
builder.responseHeader("X-Custom-Header", "42")));
}
};
}

View File

@@ -55,7 +55,7 @@ public abstract class AbstractWebGraphQLService implements WebGraphQLService {
public final Mono<WebOutput> execute(WebInput input) {
return preHandle(input)
.flatMap(executionInput -> Mono.fromFuture(executeInternal(executionInput)))
.flatMap(executionResult -> postHandle(new WebOutput(input, executionResult, null)));
.flatMap(executionResult -> postHandle(new WebOutput(input, executionResult)));
}
private Mono<ExecutionInput> preHandle(WebInput input) {

View File

@@ -21,17 +21,17 @@ import reactor.core.publisher.Mono;
/**
* Contract to execute a GraphQL request.
*
* @param <I> the GraphQL query container along with any additional context
* depending on the environment in which the request is handled
* @param <O> the result of query execution and additional environment output
* @param <IN> container for the GraphQL query along additional transport
* related context such as the HTTP url or headers for web.
* @param <OUT> the query execution result
*/
public interface GraphQLService<I extends RequestInput, O extends ExecutionResult> {
public interface GraphQLService<IN extends RequestInput, OUT extends ExecutionResult> {
/**
* Perform the request and return the result.
* @param input the GraphQL query container
* @return the execution result
*/
Mono<O> execute(I input);
Mono<OUT> execute(IN input);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2020 the original author or authors.
* 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.
@@ -30,8 +30,9 @@ import org.springframework.util.Assert;
/**
* {@link ExecutionResult} that wraps another in order to provide a convenient
* way to {@link #transform(Consumer) transform} it.
* Decorate an {@link ExecutionResult}, provide a way to
* {@link #transform(Consumer) transform} it, and collect input for custom
* HTTP response headers for GraphQL over HTTP requests.
*/
public class WebOutput implements ExecutionResult {
@@ -40,18 +41,22 @@ public class WebOutput implements ExecutionResult {
private final ExecutionResult executionResult;
@Nullable
private final HttpHeaders headers;
private final HttpHeaders responseHeaders;
/**
* Create an instance that wraps the given {@link ExecutionResult}.
*/
public WebOutput(WebInput input, ExecutionResult executionResult, @Nullable HttpHeaders headers) {
public WebOutput(WebInput input, ExecutionResult executionResult) {
this(input, executionResult, null);
}
private WebOutput(WebInput input, ExecutionResult executionResult, @Nullable HttpHeaders responseHeaders) {
Assert.notNull(input, "WebInput is required.");
Assert.notNull(executionResult, "ExecutionResult is required.");
this.input = input;
this.executionResult = executionResult;
this.headers = headers;
this.responseHeaders = responseHeaders;
}
@@ -88,11 +93,15 @@ public class WebOutput implements ExecutionResult {
}
/**
* Return headers to be added to the HTTP response.
* Return a read-only view of any custom headers to be added to the HTTP
* response, or {@code null} until {@link #transform(Consumer)} is used to
* add such headers.
* @see #transform(Consumer)
* @see Builder#responseHeader(String, String...)
*/
@Nullable
public HttpHeaders getHeaders() {
return this.headers;
public HttpHeaders getResponseHeaders() {
return (this.responseHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.responseHeaders) : null);
}
/**
@@ -106,6 +115,9 @@ public class WebOutput implements ExecutionResult {
}
/**
* Builder to transform a {@link WebOutput}.
*/
public static class Builder {
private final WebInput input;
@@ -127,12 +139,13 @@ public class WebOutput implements ExecutionResult {
this.data = output.getData();
this.errors = output.getErrors();
this.extensions = output.getExtensions();
this.headers = output.getHeaders();
this.headers = output.responseHeaders;
}
/**
* Set the execution {@link ExecutionResult#getData() data}.
* Set the {@link ExecutionResult#getData() data} of the GraphQL
* execution result.
*/
public Builder data(@Nullable Object data) {
this.data = data;
@@ -140,7 +153,8 @@ public class WebOutput implements ExecutionResult {
}
/**
* Set the execution {@link ExecutionResult#getErrors() errors}.
* Set the {@link ExecutionResult#getErrors() errors} of the GraphQL
* execution result.
*/
public Builder errors(@Nullable List<GraphQLError> errors) {
this.errors = (errors != null ? errors : Collections.emptyList());
@@ -148,14 +162,22 @@ public class WebOutput implements ExecutionResult {
}
/**
* Set the execution {@link ExecutionResult#getExtensions() extensions}.
* Set the {@link ExecutionResult#getExtensions() extensions} of the
* GraphQL execution result.
*/
public Builder extensions(@Nullable Map<Object, Object> extensions) {
this.extensions = extensions;
return this;
}
public Builder header(String name, String... values) {
/**
* Add a custom header to be set on the HTTP response.
*
* <p><strong>Note:</strong> This can be used for GraphQL over HTTP query
* requests but has no impact for queries over a WebSocket session where
* the initial handshake request completes before queries begin.
*/
public Builder responseHeader(String name, String... values) {
initHeaders();
for (String value : values) {
this.headers.add(name, value);
@@ -163,7 +185,14 @@ public class WebOutput implements ExecutionResult {
return this;
}
public Builder headers(Consumer<HttpHeaders> consumer) {
/**
* Consume and update the headers to be set on the HTTP response.
*
* <p><strong>Note:</strong> This can be used for GraphQL over HTTP query
* requests but has no impact for queries over a WebSocket session where
* the initial handshake request completes before queries begin.
*/
public Builder responseHeaders(Consumer<HttpHeaders> consumer) {
initHeaders();
consumer.accept(this.headers);
return this;

View File

@@ -71,8 +71,8 @@ public class GraphQLHttpHandler {
logger.debug("Execution complete");
}
ServerResponse.BodyBuilder builder = ServerResponse.ok();
if (output.getHeaders() != null) {
builder.headers(headers -> headers.putAll(output.getHeaders()));
if (output.getResponseHeaders() != null) {
builder.headers(headers -> headers.putAll(output.getResponseHeaders()));
}
return builder.bodyValue(spec);
});

View File

@@ -75,8 +75,8 @@ public class GraphQLHttpHandler {
logger.debug("Execution complete");
}
ServerResponse.BodyBuilder builder = ServerResponse.ok();
if (output.getHeaders() != null) {
builder.headers(headers -> headers.putAll(output.getHeaders()));
if (output.getResponseHeaders() != null) {
builder.headers(headers -> headers.putAll(output.getResponseHeaders()));
}
return builder.body(output.toSpecification());
});

View File

@@ -71,7 +71,7 @@ public class DefaultWebGraphQLServiceTests {
assertThat(sb.toString()).isEqualTo(":pre1:pre2:pre3:post3:post2:post1");
assertThat(webOutput.isDataPresent()).isTrue();
assertThat(webOutput.getHeaders().get("MyHeader")).containsExactly("MyValue3", "MyValue2", "MyValue1");
assertThat(webOutput.getResponseHeaders().get("MyHeader")).containsExactly("MyValue3", "MyValue2", "MyValue1");
}
@@ -107,12 +107,11 @@ public class DefaultWebGraphQLServiceTests {
}
@Override
public Mono<WebOutput> postHandle(WebOutput webOutput) {
public Mono<WebOutput> postHandle(WebOutput output) {
this.output.append(":post").append(this.index);
return Mono.delay(Duration.ofMillis(50))
.map(aLong -> webOutput.transform(builder -> {
builder.header("myHeader", "MyValue" + this.index);
}));
.map(aLong -> output.transform(builder ->
builder.responseHeader("myHeader", "MyValue" + this.index)));
}
}