OneOfInputObjectRules.java

package graphql.schema.validation;

import graphql.ExperimentalApi;
import graphql.schema.GraphQLInputObjectField;
import graphql.schema.GraphQLInputObjectType;
import graphql.schema.GraphQLSchemaElement;
import graphql.schema.GraphQLType;
import graphql.schema.GraphQLTypeUtil;
import graphql.schema.GraphQLTypeVisitorStub;
import graphql.schema.GraphQLUnmodifiedType;
import graphql.util.TraversalControl;
import graphql.util.TraverserContext;

import java.util.LinkedHashSet;
import java.util.Set;

import static java.lang.String.format;

/*
 * Spec: If the Input Object is a OneOf Input Object then:
 * The type of the input field must be nullable.
 * The input field must not have a default value.
 */
@ExperimentalApi
public class OneOfInputObjectRules extends GraphQLTypeVisitorStub {

    @Override
    public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType inputObjectType, TraverserContext<GraphQLSchemaElement> context) {
        if (!inputObjectType.isOneOf()) {
            return TraversalControl.CONTINUE;
        }
        SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class);
        if (!canBeProvidedAFiniteValue(inputObjectType, new LinkedHashSet<>())) {
            String message = format("OneOf Input Object %s must be inhabited but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.", inputObjectType.getName());
            errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfNotInhabited, message));
        }
        return TraversalControl.CONTINUE;
    }

    private boolean canBeProvidedAFiniteValue(GraphQLInputObjectType oneOfInputObject, Set<GraphQLInputObjectType> visited) {
        if (visited.contains(oneOfInputObject)) {
            return false;
        }
        Set<GraphQLInputObjectType> nextVisited = new LinkedHashSet<>(visited);
        nextVisited.add(oneOfInputObject);
        for (GraphQLInputObjectField field : oneOfInputObject.getFieldDefinitions()) {
            GraphQLType fieldType = field.getType();
            if (GraphQLTypeUtil.isList(fieldType)) {
                return true;
            }
            GraphQLUnmodifiedType namedFieldType = GraphQLTypeUtil.unwrapAll(fieldType);
            if (!(namedFieldType instanceof GraphQLInputObjectType)) {
                return true;
            }
            GraphQLInputObjectType inputFieldType = (GraphQLInputObjectType) namedFieldType;
            if (!inputFieldType.isOneOf()) {
                return true;
            }
            if (canBeProvidedAFiniteValue(inputFieldType, nextVisited)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inputObjectField, TraverserContext<GraphQLSchemaElement> context) {
        GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) context.getParentNode();
        if (!inputObjectType.isOneOf()) {
            return TraversalControl.CONTINUE;
        }
        SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class);
        // error messages take from the reference implementation
        if (inputObjectField.hasSetDefaultValue()) {
            String message = format("OneOf input field %s.%s cannot have a default value.", inputObjectType.getName(), inputObjectField.getName());
            errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfDefaultValueOnField, message));
        }

        if (GraphQLTypeUtil.isNonNull(inputObjectField.getType())) {
            String message = format("OneOf input field %s.%s must be nullable.", inputObjectType.getName(), inputObjectField.getName());
            errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfNonNullableField, message));
        }
        return TraversalControl.CONTINUE;
    }
}