diff --git a/src/main/java/graphql/execution/DataFetcherResult.java b/src/main/java/graphql/execution/DataFetcherResult.java
index cbf0a03639..cfe6337d0c 100644
--- a/src/main/java/graphql/execution/DataFetcherResult.java
+++ b/src/main/java/graphql/execution/DataFetcherResult.java
@@ -10,6 +10,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -24,10 +25,19 @@
* This also allows you to pass down new local context objects between parent and child fields. If you return a
* {@link #getLocalContext()} value then it will be passed down into any child fields via
* {@link graphql.schema.DataFetchingEnvironment#getLocalContext()}
- *
+ *
* You can also have {@link DataFetcher}s contribute to the {@link ExecutionResult#getExtensions()} by returning
* extensions maps that will be merged together via the {@link graphql.extensions.ExtensionsBuilder} and its {@link graphql.extensions.ExtensionsMerger}
* in place.
+ *
+ * This provides {@link #hashCode()} and {@link #equals(Object)} methods that afford comparison with other {@link DataFetcherResult} object.s
+ * However, to function correctly, this relies on the values provided in the following fields in turn also implementing {@link #hashCode()}} and {@link #equals(Object)} as appropriate:
+ *
+ *
The data returned in {@link #getData()}.
+ *
The individual errors returned in {@link #getErrors()}.
+ *
The context returned in {@link #getLocalContext()}.
+ *
The keys/values in the {@link #getExtensions()} {@link Map}.
+ *
*
* @param The type of the data fetched
*/
@@ -123,6 +133,35 @@ public DataFetcherResult map(Function transformation) {
.build();
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ DataFetcherResult> that = (DataFetcherResult>) o;
+ return Objects.equals(data, that.data)
+ && errors.equals(that.errors)
+ && Objects.equals(localContext, that.localContext)
+ && Objects.equals(extensions, that.extensions);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(data, errors, localContext, extensions);
+ }
+
+ @Override
+ public String toString() {
+ return "DataFetcherResult{" +
+ "data=" + data +
+ ", errors=" + errors +
+ ", localContext=" + localContext +
+ ", extensions=" + extensions +
+ '}';
+ }
+
/**
* Creates a new data fetcher result builder
*
diff --git a/src/test/groovy/graphql/execution/DataFetcherResultTest.groovy b/src/test/groovy/graphql/execution/DataFetcherResultTest.groovy
index 35fbfe2f1d..07318afa75 100644
--- a/src/test/groovy/graphql/execution/DataFetcherResultTest.groovy
+++ b/src/test/groovy/graphql/execution/DataFetcherResultTest.groovy
@@ -1,5 +1,6 @@
package graphql.execution
+import graphql.GraphQLError
import graphql.InvalidSyntaxError
import graphql.validation.ValidationError
import graphql.validation.ValidationErrorType
@@ -107,4 +108,77 @@ class DataFetcherResultTest extends Specification {
result.getExtensions() == [a : "b"]
result.getErrors() == [error1, error2]
}
+
+ def "implements equals/hashCode for matching results"() {
+ when:
+ def firstResult = toDataFetcherResult(first)
+ def secondResult = toDataFetcherResult(second)
+
+ then:
+ firstResult == secondResult
+ firstResult.hashCode() == secondResult.hashCode()
+
+ where:
+ first | second
+ [data: "A string"] | [data: "A string"]
+ [data: 5] | [data: 5]
+ [data: ["a", "b"]] | [data: ["a", "b"]]
+ [errors: [error("An error")]] | [errors: [error("An error")]]
+ [data: "A value", errors: [error("An error")]] | [data: "A value", errors: [error("An error")]]
+ [data: "A value", localContext: 5] | [data: "A value", localContext: 5]
+ [data: "A value", errors: [error("An error")], localContext: 5] | [data: "A value", errors: [error("An error")], localContext: 5]
+ [data: "A value", extensions: ["key": "value"]] | [data: "A value", extensions: ["key": "value"]]
+ [data: "A value", errors: [error("An error")], localContext: 5, extensions: ["key": "value"]] | [data: "A value", errors: [error("An error")], localContext: 5, extensions: ["key": "value"]]
+ }
+
+ def "implements equals/hashCode for different results"() {
+ when:
+ def firstResult = toDataFetcherResult(first)
+ def secondResult = toDataFetcherResult(second)
+
+ then:
+ firstResult != secondResult
+ firstResult.hashCode() != secondResult.hashCode()
+
+ where:
+ first | second
+ [data: "A string"] | [data: "A different string"]
+ [data: 5] | [data: "not 5"]
+ [data: ["a", "b"]] | [data: ["a", "c"]]
+ [errors: [error("An error")]] | [errors: [error("A different error")]]
+ [data: "A value", errors: [error("An error")]] | [data: "A different value", errors: [error("An error")]]
+ [data: "A value", localContext: 5] | [data: "A value", localContext: 1]
+ [data: "A value", errors: [error("An error")], localContext: 5] | [data: "A value", errors: [error("A different error")], localContext: 5]
+ [data: "A value", extensions: ["key": "value"]] | [data: "A value", extensions: ["key", "different value"]]
+ [data: "A value", errors: [error("An error")], localContext: 5, extensions: ["key": "value"]] | [data: "A value", errors: [error("An error")], localContext: 5, extensions: ["key": "different value"]]
+ }
+
+ private static DataFetcherResult toDataFetcherResult(Map resultFields) {
+ def resultBuilder = DataFetcherResult.newResult();
+ resultFields.forEach { key, value ->
+ if (value != null) {
+ switch (key) {
+ case "data":
+ resultBuilder.data(value)
+ break;
+ case "errors":
+ resultBuilder.errors(value as List);
+ break;
+ case "localContext":
+ resultBuilder.localContext(value);
+ break;
+ case "extensions":
+ resultBuilder.extensions(value as Map