An Extensions Builder by bbakerman · Pull Request #3049 · graphql-java/graphql-java · GitHub
Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/main/java/graphql/extensions/DefaultExtensionsMerger.java
110 changes: 110 additions & 0 deletions src/main/java/graphql/extensions/ExtensionsBuilder.java
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()));
}
}
45 changes: 45 additions & 0 deletions src/main/java/graphql/extensions/ExtensionsMerger.java
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);
}
Loading