graphql.analysis.values.ValueTraverser Maven / Gradle / Ivy
package graphql.analysis.values;
import com.google.common.collect.ImmutableList;
import graphql.PublicApi;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.DataFetchingEnvironmentImpl;
import graphql.schema.GraphQLAppliedDirective;
import graphql.schema.GraphQLAppliedDirectiveArgument;
import graphql.schema.GraphQLArgument;
import graphql.schema.GraphQLEnumType;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLInputObjectField;
import graphql.schema.GraphQLInputObjectType;
import graphql.schema.GraphQLInputSchemaElement;
import graphql.schema.GraphQLInputType;
import graphql.schema.GraphQLInputValueDefinition;
import graphql.schema.GraphQLList;
import graphql.schema.GraphQLNonNull;
import graphql.schema.GraphQLScalarType;
import graphql.schema.GraphQLTypeUtil;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static graphql.Assert.assertShouldNeverHappen;
import static graphql.Assert.assertTrue;
import static graphql.analysis.values.ValueVisitor.ABSENCE_SENTINEL;
/**
* This class allows you to traverse a set of input values according to the type system and optional
* change the values present.
*
* If you just want to traverse without changing anything, just return the value presented to you and nothing will change.
*
* If you want to change a value, perhaps in the presence of a directive say on the containing element, then return
* a new value back in your visitor.
*
* This class is intended to be used say inside a DataFetcher, allowing you to change the {@link DataFetchingEnvironment#getArguments()}
* say before further processing.
*
* The values passed in are assumed to be valid and coerced. This classes does not check for non nullness say or the right coerced objects given
* the type system. This is assumed to have occurred earlier in the graphql validation phase. This also means if you are not careful you can undo the
* validation that has gone before you. For example, it would be possible to change values that are illegal according to the type system, such as
* null values for non-nullable types say, so you need to be careful.
*/
@PublicApi
public class ValueTraverser {
private static class InputElements implements ValueVisitor.InputElements {
private final ImmutableList inputElements;
private final List unwrappedInputElements;
private final GraphQLInputValueDefinition lastElement;
private InputElements(GraphQLInputSchemaElement startElement) {
this.inputElements = ImmutableList.of(startElement);
this.unwrappedInputElements = ImmutableList.of(startElement);
this.lastElement = startElement instanceof GraphQLInputValueDefinition ? (GraphQLInputValueDefinition) startElement : null;
}
private InputElements(ImmutableList inputElements) {
this.inputElements = inputElements;
this.unwrappedInputElements = inputElements.stream()
.filter(it -> !(it instanceof GraphQLNonNull || it instanceof GraphQLList))
.collect(ImmutableList.toImmutableList());
List inputValDefs = unwrappedInputElements.stream()
.filter(it -> it instanceof GraphQLInputValueDefinition)
.map(GraphQLInputValueDefinition.class::cast).collect(Collectors.toList());
this.lastElement = inputValDefs.isEmpty() ? null : inputValDefs.get(inputValDefs.size() - 1);
}
private InputElements push(GraphQLInputSchemaElement inputElement) {
ImmutableList newSchemaElements = ImmutableList.builder()
.addAll(inputElements).add(inputElement).build();
return new InputElements(newSchemaElements);
}
@Override
public List getInputElements() {
return inputElements;
}
public List getUnwrappedInputElements() {
return unwrappedInputElements;
}
@Override
public GraphQLInputValueDefinition getLastInputValueDefinition() {
return lastElement;
}
}
/**
* This will visit the arguments of a {@link DataFetchingEnvironment} and if the values are changed by the visitor a new environment will be built
*
* @param environment the starting data fetching environment
* @param visitor the visitor to use
*
* @return the same environment if nothing changes or a new one with the {@link DataFetchingEnvironment#getArguments()} changed
*/
public static DataFetchingEnvironment visitPreOrder(DataFetchingEnvironment environment, ValueVisitor visitor) {
GraphQLFieldDefinition fieldDefinition = environment.getFieldDefinition();
Map originalArgs = environment.getArguments();
Map newArgs = visitPreOrder(originalArgs, fieldDefinition, visitor);
if (newArgs != originalArgs) {
return DataFetchingEnvironmentImpl.newDataFetchingEnvironment(environment).arguments(newArgs).build();
}
return environment;
}
/**
* This will visit the arguments of a {@link GraphQLFieldDefinition} and if the visitor changes the values, it will return a new set of arguments
*
* @param coercedArgumentValues the starting coerced arguments
* @param fieldDefinition the field definition
* @param visitor the visitor to use
*
* @return the same set of arguments if nothing changes or new ones if the visitor changes anything
*/
public static Map visitPreOrder(Map coercedArgumentValues, GraphQLFieldDefinition fieldDefinition, ValueVisitor visitor) {
List fieldArguments = fieldDefinition.getArguments();
boolean copied = false;
for (GraphQLArgument fieldArgument : fieldArguments) {
String key = fieldArgument.getName();
Object argValue = coercedArgumentValues.get(key);
InputElements inputElements = new InputElements(fieldArgument);
Object newValue = visitor.visitArgumentValue(argValue, fieldArgument, inputElements);
if (hasChanged(newValue, argValue)) {
if (!copied) {
coercedArgumentValues = new LinkedHashMap<>(coercedArgumentValues);
copied = true;
}
setNewValue(coercedArgumentValues, key, newValue);
}
if (newValue != ABSENCE_SENTINEL) {
newValue = visitPreOrderImpl(argValue, fieldArgument.getType(), inputElements, visitor);
if (hasChanged(newValue, argValue)) {
if (!copied) {
coercedArgumentValues = new LinkedHashMap<>(coercedArgumentValues);
copied = true;
}
setNewValue(coercedArgumentValues, key, newValue);
}
}
}
return coercedArgumentValues;
}
/**
* This will visit a single argument of a {@link GraphQLArgument} and if the visitor changes the value, it will return a new argument value
*
* Note you cannot return the ABSENCE_SENTINEL from this method as its makes no sense to be somehow make the argument disappear. Use
* {@link #visitPreOrder(Map, GraphQLFieldDefinition, ValueVisitor)} say to remove arguments in the fields map of arguments.
*
* @param coercedArgumentValue the starting coerced argument value
* @param argument the argument definition
* @param visitor the visitor to use
*
* @return the same value if nothing changes or a new value if the visitor changes anything
*/
public static Object visitPreOrder(Object coercedArgumentValue, GraphQLArgument argument, ValueVisitor visitor) {
InputElements inputElements = new InputElements(argument);
Object newValue = visitor.visitArgumentValue(coercedArgumentValue, argument, inputElements);
if (newValue == ABSENCE_SENTINEL) {
assertShouldNeverHappen("It makes no sense to return the ABSENCE_SENTINEL during the visitPreOrder GraphQLArgument method");
}
newValue = visitPreOrderImpl(newValue, argument.getType(), inputElements, visitor);
if (newValue == ABSENCE_SENTINEL) {
assertShouldNeverHappen("It makes no sense to return the ABSENCE_SENTINEL during the visitPreOrder GraphQLArgument method");
}
return newValue;
}
/**
* This will visit a single argument of a {@link GraphQLAppliedDirective} and if the visitor changes the value, it will return a new argument value
*
* Note you cannot return the ABSENCE_SENTINEL from this method as its makes no sense to be somehow make the argument disappear.
*
* @param coercedArgumentValue the starting coerced argument value
* @param argument the applied argument
* @param visitor the visitor to use
*
* @return the same value if nothing changes or a new value if the visitor changes anything
*/
public static Object visitPreOrder(Object coercedArgumentValue, GraphQLAppliedDirectiveArgument argument, ValueVisitor visitor) {
InputElements inputElements = new InputElements(argument);
Object newValue = visitor.visitAppliedDirectiveArgumentValue(coercedArgumentValue, argument, inputElements);
if (newValue == ABSENCE_SENTINEL) {
assertShouldNeverHappen("It makes no sense to return the ABSENCE_SENTINEL during the visitPreOrder GraphQLAppliedDirectiveArgument method");
}
newValue = visitPreOrderImpl(newValue, argument.getType(), inputElements, visitor);
if (newValue == ABSENCE_SENTINEL) {
assertShouldNeverHappen("It makes no sense to return the ABSENCE_SENTINEL during the visitPreOrder GraphQLAppliedDirectiveArgument method");
}
return newValue;
}
private static Object visitPreOrderImpl(Object coercedValue, GraphQLInputType startingInputType, InputElements containingElements, ValueVisitor visitor) {
if (startingInputType instanceof GraphQLNonNull) {
containingElements = containingElements.push(startingInputType);
}
GraphQLInputType inputType = GraphQLTypeUtil.unwrapNonNullAs(startingInputType);
containingElements = containingElements.push(inputType);
if (inputType instanceof GraphQLList) {
return visitListValue(coercedValue, (GraphQLList) inputType, containingElements, visitor);
} else if (inputType instanceof GraphQLInputObjectType) {
GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) inputType;
return visitObjectValue(coercedValue, inputObjectType, containingElements, visitor);
} else if (inputType instanceof GraphQLScalarType) {
return visitor.visitScalarValue(coercedValue, (GraphQLScalarType) inputType, containingElements);
} else if (inputType instanceof GraphQLEnumType) {
return visitor.visitEnumValue(coercedValue, (GraphQLEnumType) inputType, containingElements);
} else {
return assertShouldNeverHappen("ValueTraverser can only be called on full materialised schemas");
}
}
private static Object visitObjectValue(Object coercedValue, GraphQLInputObjectType inputObjectType, InputElements containingElements, ValueVisitor visitor) {
if (coercedValue != null) {
assertTrue(coercedValue instanceof Map, () -> "A input object type MUST have an Map value");
}
@SuppressWarnings("unchecked")
Map map = (Map) coercedValue;
Map newMap = visitor.visitInputObjectValue(map, inputObjectType, containingElements);
if (newMap == ABSENCE_SENTINEL) {
return ABSENCE_SENTINEL;
}
if (newMap != null) {
boolean copied = false;
for (Map.Entry entry : newMap.entrySet()) {
String key = entry.getKey();
GraphQLInputObjectField inputField = inputObjectType.getField(key);
/// should we assert if the map contain a key that's not a field ?
if (inputField != null) {
InputElements inputElementsWithField = containingElements.push(inputField);
Object newValue = visitor.visitInputObjectFieldValue(entry.getValue(), inputObjectType, inputField, inputElementsWithField);
if (hasChanged(newValue, entry.getValue())) {
if (!copied) {
newMap = new LinkedHashMap<>(newMap);
copied = true;
}
setNewValue(newMap, key, newValue);
}
// if the value has gone - then we cant descend into it
if (newValue != ABSENCE_SENTINEL) {
newValue = visitPreOrderImpl(newValue, inputField.getType(), inputElementsWithField, visitor);
if (hasChanged(newValue, entry.getValue())) {
if (!copied) {
newMap = new LinkedHashMap<>(newMap);
copied = true;
}
setNewValue(newMap, key, newValue);
}
}
}
}
return newMap;
} else {
return null;
}
}
private static Object visitListValue(Object coercedValue, GraphQLList listInputType, InputElements containingElements, ValueVisitor visitor) {
if (coercedValue != null) {
assertTrue(coercedValue instanceof List, () -> "A list type MUST have an List value");
}
@SuppressWarnings("unchecked")
List