Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add class hierarchy for incremental execution result #3414

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 21 additions & 17 deletions src/main/java/graphql/ExecutionResultImpl.java
Expand Up @@ -40,6 +40,10 @@ public ExecutionResultImpl(ExecutionResultImpl other) {
this(other.dataPresent, other.data, other.errors, other.extensions);
}

public <T extends Builder<T>> ExecutionResultImpl(Builder<T> builder) {
this(builder.dataPresent, builder.data, builder.errors, builder.extensions);
}

private ExecutionResultImpl(boolean dataPresent, Object data, List<? extends GraphQLError> errors, Map<Object, Object> extensions) {
this.dataPresent = dataPresent;
this.data = data;
Expand Down Expand Up @@ -103,61 +107,61 @@ public String toString() {
'}';
}

public static Builder newExecutionResult() {
return new Builder();
public static <T extends Builder<T>> Builder<T> newExecutionResult() {
return new Builder<>();
}

public static class Builder implements ExecutionResult.Builder<Builder> {
public static class Builder<T extends Builder<T>> implements ExecutionResult.Builder<T> {
private boolean dataPresent;
private Object data;
private List<GraphQLError> errors = new ArrayList<>();
private Map<Object, Object> extensions;

@Override
public Builder from(ExecutionResult executionResult) {
public T from(ExecutionResult executionResult) {
dataPresent = executionResult.isDataPresent();
data = executionResult.getData();
errors = new ArrayList<>(executionResult.getErrors());
extensions = executionResult.getExtensions();
return this;
return (T) this;
}

@Override
public Builder data(Object data) {
public T data(Object data) {
dataPresent = true;
this.data = data;
return this;
return (T) this;
}

@Override
public Builder errors(List<GraphQLError> errors) {
public T errors(List<GraphQLError> errors) {
this.errors = errors;
return this;
return (T) this;
}

@Override
public Builder addErrors(List<GraphQLError> errors) {
public T addErrors(List<GraphQLError> errors) {
this.errors.addAll(errors);
return this;
return (T) this;
}

@Override
public Builder addError(GraphQLError error) {
public T addError(GraphQLError error) {
this.errors.add(error);
return this;
return (T) this;
}

@Override
public Builder extensions(Map<Object, Object> extensions) {
public T extensions(Map<Object, Object> extensions) {
this.extensions = extensions;
return this;
return (T) this;
}

@Override
public Builder addExtension(String key, Object value) {
public T addExtension(String key, Object value) {
this.extensions = (this.extensions == null ? new LinkedHashMap<>() : this.extensions);
this.extensions.put(key, value);
return this;
return (T) this;
}

@Override
Expand Down
72 changes: 72 additions & 0 deletions src/main/java/graphql/incremental/DeferPayload.java
@@ -0,0 +1,72 @@
package graphql.incremental;

import graphql.ExperimentalApi;
import graphql.GraphQLError;

import javax.annotation.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Represents a defer payload
*/
@ExperimentalApi
public class DeferPayload extends IncrementalPayload {
private final Object data;

private DeferPayload(Object data, List<Object> path, String label, List<GraphQLError> errors, Map<Object, Object> extensions) {
super(path, label, errors, extensions);
this.data = data;
}

/**
* @return the resolved data
* @param <T> the type to cast the result to
*/
@Nullable
public <T> T getData() {
//noinspection unchecked
return (T) this.data;
}

/**
* @return a map of this payload that strictly follows the spec
*/
@Override
public Map<String, Object> toSpecification() {
Map<String, Object> map = new LinkedHashMap<>(super.toSpecification());

if (data != null) {
map.put("data", data);
}

return map;
}

/**
* @return a {@link DeferPayload.Builder} that can be used to create an instance of {@link DeferPayload}
*/
public static DeferPayload.Builder newDeferredItem() {
return new DeferPayload.Builder();
}

public static class Builder extends IncrementalPayload.Builder<Builder> {
private Object data = null;

public Builder data(Object data) {
this.data = data;
return this;
}

public Builder from(DeferPayload deferredItem) {
super.from(deferredItem);
this.data = deferredItem.data;
return this;
}

public DeferPayload build() {
return new DeferPayload(data, this.path, this.label, this.errors, this.extensions);
}
}
}
@@ -0,0 +1,38 @@
package graphql.incremental;

import graphql.ExperimentalApi;

import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;

/**
* Represents a result that is delivered asynchronously, after the initial {@link IncrementalExecutionResult}.
* <p>
* Multiple defer and/or stream payloads (represented by {@link IncrementalPayload}) can be part of the same
* {@link DelayedIncrementalExecutionResult}
*/
@ExperimentalApi
public interface DelayedIncrementalExecutionResult {
/**
* @return a list of defer and/or stream payloads.
*/
@Nullable
List<IncrementalPayload> getIncremental();

/**
* Indicates whether the stream will continue emitting {@link DelayedIncrementalExecutionResult}s after this one.
* <p>
* The value returned by this method should be "true" for all but the last response in the stream. The value of this
* entry is `false` for the last response of the stream.
*
* @return "true" if there are more responses in the stream, "false" otherwise.
*/
boolean hasNext();

/**
* @return a map of extensions or null if there are none
*/
@Nullable
Map<Object, Object> getExtensions();
}
@@ -0,0 +1,67 @@
package graphql.incremental;

import graphql.ExperimentalApi;

import java.util.Collections;
import java.util.List;
import java.util.Map;

@ExperimentalApi
public class DelayedIncrementalExecutionResultImpl implements DelayedIncrementalExecutionResult {
private final List<IncrementalPayload> incrementalItems;
private final boolean hasNext;
private final Map<Object, Object> extensions;

private DelayedIncrementalExecutionResultImpl(Builder builder) {
this.incrementalItems = builder.incrementalItems;
this.hasNext = builder.hasNext;
this.extensions = builder.extensions;
}

@Override
public List<IncrementalPayload> getIncremental() {
return this.incrementalItems;
}

@Override
public boolean hasNext() {
return this.hasNext;
}

@Override
public Map<Object, Object> getExtensions() {
return this.extensions;
}

/**
* @return a {@link Builder} that can be used to create an instance of {@link DelayedIncrementalExecutionResultImpl}
*/
public static Builder newIncrementalExecutionResult() {
return new Builder();
}

public static class Builder {
private boolean hasNext = false;
private List<IncrementalPayload> incrementalItems = Collections.emptyList();
private Map<Object, Object> extensions;

public Builder hasNext(boolean hasNext) {
this.hasNext = hasNext;
return this;
}

public Builder incrementalItems(List<IncrementalPayload> incrementalItems) {
this.incrementalItems = incrementalItems;
return this;
}

public Builder extensions(boolean hasNext) {
this.hasNext = hasNext;
return this;
}

public DelayedIncrementalExecutionResultImpl build() {
return new DelayedIncrementalExecutionResultImpl(this);
}
}
}
111 changes: 111 additions & 0 deletions src/main/java/graphql/incremental/IncrementalExecutionResult.java
@@ -0,0 +1,111 @@
package graphql.incremental;

import graphql.ExecutionResult;
import graphql.ExperimentalApi;
import org.reactivestreams.Publisher;

import javax.annotation.Nullable;
import java.util.List;

/**
* A result that is part of an execution that includes incrementally delivered data (data has been deferred of streamed).
* <p>
* For example, this query
* <pre>
* query {
* person(id: "cGVvcGxlOjE=") {
* ...HomeWorldFragment @defer(label: "homeWorldDefer")
* name
* films @stream(initialCount: 1, label: "filmsStream") {
* title
* }
* }
* }
* fragment HomeWorldFragment on Person {
* homeWorld {
* name
* }
* }
* </pre>
* Could result on an incremental response with the following payloads (in JSON format here for simplicity).
* <p>
* <b>Response 1, the initial response does not contain any deferred or streamed results.</b>
* <pre>
* {
* "data": {
* "person": {
* "name": "Luke Skywalker",
* "films": [{ "title": "A New Hope" }]
* }
* },
* "hasNext": true
* }
* </pre>
*
* <b>Response 2, contains the defer payload and the first stream payload.</b>
* <pre>
* {
* "incremental": [
* {
* "label": "homeWorldDefer",
* "path": ["person"],
* "data": { "homeWorld": { "name": "Tatooine" } }
* },
* {
* "label": "filmsStream",
* "path": ["person", "films", 1],
* "items": [{ "title": "The Empire Strikes Back" }]
* }
* ],
* "hasNext": true
* }
* </pre>
*
* <b>Response 3, contains the final stream payload. Note how "hasNext" is "false", indicating this is the final response.</b>
* <pre>
* {
* "incremental": [
* {
* "label": "filmsStream",
* "path": ["person", "films", 2],
* "items": [{ "title": "Return of the Jedi" }]
* }
* ],
* "hasNext": false
* }
* </pre>
*
* <p>
* This implementation is based on the state of <a href="https://github.com/graphql/graphql-spec/pull/742">Defer/Stream PR</a>
* More specifically at the state of this
* <a href="https://github.com/graphql/graphql-spec/commit/c630301560d9819d33255d3ba00f548e8abbcdc6">commit</a>
* <p>
* The execution behaviour should match what we get from running Apollo Server 4.9.5 with graphql-js v17.0.0-alpha.2
*/
@ExperimentalApi
public interface IncrementalExecutionResult extends ExecutionResult {
/**
* Indicates whether there are pending incremental data.
* @return "true" if there are incremental data, "false" otherwise.
*/
boolean hasNext();

/**
* Returns a list of defer and/or stream payloads that the execution engine decided (for whatever reason) to resolve at the same time as the initial payload.
* <p>
* (...)this field may appear on both the initial and subsequent values.
* <p>
* <a href="https://github.com/graphql/graphql-spec/pull/742/files#diff-98d0cd153b72b63c417ad4238e8cc0d3385691ccbde7f7674bc0d2a718b896ecR271">source</a>
*
* @return a list of Stream and/or Defer payloads that were resolved at the same time as the initial payload.
*/
@Nullable
List<IncrementalPayload> getIncremental();

/**
* This {@link Publisher} will asynchronously emit events containing defer and/or stream payloads.
*
* @return a {@link Publisher} that clients can subscribe to receive incremental payloads.
*/
Publisher<DelayedIncrementalExecutionResult> getIncrementalItemPublisher();
}