Skip to content
Navigation Menu
{{ message }}
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
An Extensions Builder #3049
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
Merged
Merged
An Extensions Builder #3049
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f42120e
Extensions Builder
bbakerman cf7e7b8
Extensions Builder - more work - javadoc and more tests
bbakerman 0ce4f86
Extensions Builder - more work - javadoc and more tests
bbakerman d1047c4
Extensions Builder - more work - integration test
bbakerman caeb6a5
Extensions Builder - more work - integration test in java style
bbakerman 9ce6c8d
Extensions Builder - more work - made it use Object/Object
bbakerman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
74 changes: 74 additions & 0 deletions
74
src/main/java/graphql/extensions/DefaultExtensionsMerger.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package graphql.extensions; | ||
|
|
||
| import com.google.common.collect.Sets; | ||
| import graphql.Internal; | ||
| import org.jetbrains.annotations.NotNull; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Collection; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
|
|
||
|
|
||
| @Internal | ||
| public class DefaultExtensionsMerger implements ExtensionsMerger { | ||
| @Override | ||
| @NotNull | ||
| public Map<Object, Object> merge(@NotNull Map<Object, Object> leftMap, @NotNull Map<Object, Object> rightMap) { | ||
| if (leftMap.isEmpty()) { | ||
| return mapCast(rightMap); | ||
| } | ||
| if (rightMap.isEmpty()) { | ||
| return mapCast(leftMap); | ||
| } | ||
| Map<Object, Object> targetMap = new LinkedHashMap<>(); | ||
| Set<Object> leftKeys = leftMap.keySet(); | ||
| for (Object key : leftKeys) { | ||
| Object leftVal = leftMap.get(key); | ||
| if (rightMap.containsKey(key)) { | ||
| Object rightVal = rightMap.get(key); | ||
| targetMap.put(key, mergeObjects(leftVal, rightVal)); | ||
| } else { | ||
| targetMap.put(key, leftVal); | ||
| } | ||
| } | ||
| Sets.SetView<Object> rightOnlyKeys = Sets.difference(rightMap.keySet(), leftKeys); | ||
| for (Object key : rightOnlyKeys) { | ||
| Object rightVal = rightMap.get(key); | ||
| targetMap.put(key, rightVal); | ||
| } | ||
| return targetMap; | ||
| } | ||
|
|
||
| private Object mergeObjects(Object leftVal, Object rightVal) { | ||
| if (leftVal instanceof Map && rightVal instanceof Map) { | ||
| return merge(mapCast(leftVal), mapCast(rightVal)); | ||
| } else if (leftVal instanceof Collection && rightVal instanceof Collection) { | ||
| // we append - no equality or merging here | ||
| return appendLists(leftVal, rightVal); | ||
| } else { | ||
| // we have some primitive - so prefer the right since it was encountered last | ||
| // and last write wins here | ||
| return rightVal; | ||
| } | ||
| } | ||
|
|
||
| @NotNull | ||
| private List<Object> appendLists(Object leftVal, Object rightVal) { | ||
| List<Object> target = new ArrayList<>(listCast(leftVal)); | ||
| target.addAll(listCast(rightVal)); | ||
| return target; | ||
| } | ||
|
|
||
| private Map<Object, Object> mapCast(Object map) { | ||
| //noinspection unchecked | ||
| return (Map<Object, Object>) map; | ||
| } | ||
|
|
||
| private Collection<Object> listCast(Object collection) { | ||
| //noinspection unchecked | ||
| return (Collection<Object>) collection; | ||
| } | ||
| } |
110 changes: 110 additions & 0 deletions
110
src/main/java/graphql/extensions/ExtensionsBuilder.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| package graphql.extensions; | ||
|
|
||
| import com.google.common.collect.ImmutableMap; | ||
| import graphql.ExecutionResult; | ||
| import graphql.PublicApi; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jetbrains.annotations.Nullable; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.CopyOnWriteArrayList; | ||
|
|
||
| import static graphql.Assert.assertNotNull; | ||
|
|
||
| /** | ||
| * This class can be used to help build the graphql `extensions` map. A series of changes to the extensions can | ||
| * be added and these will be merged together via a {@link ExtensionsMerger} implementation and that resultant | ||
| * map can be used as the `extensions` | ||
| */ | ||
| @PublicApi | ||
| public class ExtensionsBuilder { | ||
|
|
||
| // thread safe since there can be many changes say in DFs across threads | ||
| private final List<Map<Object, Object>> changes = new CopyOnWriteArrayList<>(); | ||
| private final ExtensionsMerger extensionsMerger; | ||
|
|
||
|
|
||
| private ExtensionsBuilder(ExtensionsMerger extensionsMerger) { | ||
| this.extensionsMerger = extensionsMerger; | ||
| } | ||
|
|
||
| /** | ||
| * @return a new ExtensionsBuilder using a default merger | ||
| */ | ||
| public static ExtensionsBuilder newExtensionsBuilder() { | ||
| return new ExtensionsBuilder(ExtensionsMerger.DEFAULT); | ||
| } | ||
|
|
||
| /** | ||
| * This creates a new ExtensionsBuilder with the provided {@link ExtensionsMerger} | ||
| * | ||
| * @param extensionsMerger the merging code to use | ||
| * | ||
| * @return a new ExtensionsBuilder using the provided merger | ||
| */ | ||
| public static ExtensionsBuilder newExtensionsBuilder(ExtensionsMerger extensionsMerger) { | ||
| return new ExtensionsBuilder(extensionsMerger); | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Adds new values into the extension builder | ||
| * | ||
| * @param newValues the new values to add | ||
| * | ||
| * @return this builder for fluent style reasons | ||
| */ | ||
| public ExtensionsBuilder addValues(@NotNull Map<Object, Object> newValues) { | ||
| assertNotNull(newValues); | ||
| changes.add(newValues); | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Adds a single new value into the extension builder | ||
| * | ||
| * @param key the key in the extensions | ||
| * @param value the value in the extensions | ||
| * | ||
| * @return this builder for fluent style reasons | ||
| */ | ||
| public ExtensionsBuilder addValue(@NotNull Object key, @Nullable Object value) { | ||
| assertNotNull(key); | ||
| return addValues(Collections.singletonMap(key, value)); | ||
| } | ||
|
|
||
| /** | ||
| * This builds an extensions map from this builder, merging together the values provided | ||
| * | ||
| * @return a new extensions map | ||
| */ | ||
| public Map<Object, Object> buildExtensions() { | ||
| if (changes.isEmpty()) { | ||
| return ImmutableMap.of(); | ||
| } | ||
| Map<Object, Object> firstChange = changes.get(0); | ||
| if (changes.size() == 1) { | ||
| return firstChange; | ||
| } | ||
| Map<Object, Object> outMap = new LinkedHashMap<>(firstChange); | ||
| for (int i = 1; i < changes.size(); i++) { | ||
| Map<Object, Object> newMap = extensionsMerger.merge(outMap, changes.get(i)); | ||
| assertNotNull(outMap, () -> "You MUST provide a non null Map from ExtensionsMerger.merge()"); | ||
| outMap = newMap; | ||
| } | ||
| return outMap; | ||
| } | ||
|
|
||
| /** | ||
| * This sets new extensions into the provided {@link ExecutionResult}, overwriting any previous values | ||
| * | ||
| * @return a new ExecutionResult with the extensions values in this builder | ||
| */ | ||
| public ExecutionResult setExtensions(ExecutionResult executionResult) { | ||
| assertNotNull(executionResult); | ||
| return executionResult.transform(builder -> builder.extensions(buildExtensions())); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package graphql.extensions; | ||
|
|
||
| import graphql.PublicSpi; | ||
| import org.jetbrains.annotations.NotNull; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * This interface is a callback asking code to merge two maps with an eye to creating | ||
| * the graphql `extensions` value. | ||
| * <p> | ||
| * How best to merge two maps is hard to know up front. Should it be a shallow clone or a deep one, | ||
| * should keys be replaced or not and should lists of value be combined? The {@link ExtensionsMerger} is the | ||
| * interface asked to do this. | ||
| * <p> | ||
| * This interface will be called repeatedly for each change that has been added to the {@link ExtensionsBuilder} and it is expected to merge the two maps as it sees fit | ||
| */ | ||
| @PublicSpi | ||
| public interface ExtensionsMerger { | ||
|
|
||
| /** | ||
| * A default implementation will do the following | ||
| * <ul> | ||
| * <li>It will deep merge the maps</li> | ||
| * <li>It concatenate lists when they occur under the same key</li> | ||
| * <li>It will add any keys from the right hand side map that are not present in the left</li> | ||
| * <li>If a key is in both the left and right side, it will prefer the right hand side</li> | ||
| * <li>It will try to maintain key order if the maps are ordered</li> | ||
| * </ul> | ||
| */ | ||
| ExtensionsMerger DEFAULT = new DefaultExtensionsMerger(); | ||
|
|
||
| /** | ||
| * Called to merge the map on the left with the map on the right according to whatever code strategy some-one might envisage | ||
| * <p> | ||
| * The map on the left is guaranteed to have been encountered before the map on the right | ||
| * | ||
| * @param leftMap the map on the left | ||
| * @param rightMap the map on the right | ||
| * | ||
| * @return a non null merged map | ||
| */ | ||
| @NotNull | ||
| Map<Object, Object> merge(@NotNull Map<Object, Object> leftMap, @NotNull Map<Object, Object> rightMap); | ||
| } |
79 changes: 79 additions & 0 deletions
79
src/test/groovy/graphql/extensions/DefaultExtensionsMergerTest.groovy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package graphql.extensions | ||
|
|
||
| import com.google.common.collect.ImmutableMap | ||
| import spock.lang.Specification | ||
|
|
||
| class DefaultExtensionsMergerTest extends Specification { | ||
|
|
||
| def merger = new DefaultExtensionsMerger() | ||
|
|
||
| def "can merge maps"() { | ||
|
|
||
| when: | ||
| def actual = merger.merge(leftMap, rightMap) | ||
| then: | ||
| actual == expected | ||
| where: | ||
| leftMap | rightMap | expected | ||
| [:] | [:] | ImmutableMap.of() | ||
| ImmutableMap.of() | ImmutableMap.of() | ImmutableMap.of() | ||
| // additive | ||
| [x: [firstName: "Brad"]] | [y: [lastName: "Baker"]] | [x: [firstName: "Brad"], y: [lastName: "Baker"]] | ||
| [x: "24", y: "25", z: "26"] | [a: "1", b: "2", c: "3"] | [x: "24", y: "25", z: "26", a: "1", b: "2", c: "3"] | ||
| // merge | ||
| [key1: [firstName: "Brad"]] | [key1: [lastName: "Baker"]] | [key1: [firstName: "Brad", lastName: "Baker"]] | ||
|
|
||
| // merge with right extra key | ||
| [key1: [firstName: "Brad", middleName: "Leon"]] | [key1: [lastName: "Baker"], key2: [hobby: "graphql-java"]] | [key1: [firstName: "Brad", middleName: "Leon", lastName: "Baker"], key2: [hobby: "graphql-java"]] | ||
|
|
||
| } | ||
|
|
||
| def "can handle null entries"() { | ||
|
|
||
| when: | ||
| def actual = merger.merge(leftMap, rightMap) | ||
| then: | ||
| actual == expected | ||
| where: | ||
| leftMap | rightMap | expected | ||
| // nulls | ||
| [x: [firstName: "Brad"]] | [y: [lastName: null]] | [x: [firstName: "Brad"], y: [lastName: null]] | ||
| } | ||
|
|
||
| def "prefers the right on conflict"() { | ||
|
|
||
| when: | ||
| def actual = merger.merge(leftMap, rightMap) | ||
| then: | ||
| actual == expected | ||
| where: | ||
| leftMap | rightMap | expected | ||
| [x: [firstName: "Brad"]] | [x: [firstName: "Donna"]] | [x: [firstName: "Donna"]] | ||
| [x: [firstName: "Brad"]] | [x: [firstName: "Donna", seenStarWars: true]] | [x: [firstName: "Donna", seenStarWars: true]] | ||
| [x: [firstName: "Brad", hates: "Python"]] | [x: [firstName: "Donna", seenStarWars: true]] | [x: [firstName: "Donna", hates: "Python", seenStarWars: true]] | ||
|
|
||
|
|
||
| // disparate types dont matter - it prefers the right | ||
| [x: [firstName: "Brad"]] | [x: [firstName: [salutation: "Queen", name: "Donna"]]] | [x: [firstName: [salutation: "Queen", name: "Donna"]]] | ||
|
|
||
| } | ||
|
|
||
| def "it appends to lists"() { | ||
|
|
||
| when: | ||
| def actual = merger.merge(leftMap, rightMap) | ||
| then: | ||
| actual == expected | ||
| where: | ||
| leftMap | rightMap | expected | ||
| [x: [1, 2, 3, 4]] | [x: [5, 6, 7, 8]] | [x: [1, 2, 3, 4, 5, 6, 7, 8]] | ||
| // | ||
| // truly additive - no object equality | ||
| [x: [1, 2, 3]] | [x: [1, 2, 3]] | [x: [1, 2, 3, 1, 2, 3]] | ||
| [x: []] | [x: [1, 2, 3]] | [x: [1, 2, 3]] | ||
| [x: [null]] | [x: [1, 2, 3]] | [x: [null, 1, 2, 3]] | ||
| // | ||
| // prefers right if they are not both lists | ||
| [x: null] | [x: [1, 2, 3]] | [x: [1, 2, 3]] | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
You can’t perform that action at this time.

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤣 you know me too well