All Downloads are FREE. Search and download functionalities are using the official Maven repository.

graphql.validation.rules.OverlappingFieldsCanBeMerged Maven / Gradle / Ivy

There is a newer version: 230521-nf-execution
Show newest version
package graphql.validation.rules;


import graphql.execution.TypeFromAST;
import graphql.language.Argument;
import graphql.language.AstComparator;
import graphql.language.Field;
import graphql.language.FragmentDefinition;
import graphql.language.FragmentSpread;
import graphql.language.InlineFragment;
import graphql.language.Selection;
import graphql.language.SelectionSet;
import graphql.language.Value;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLFieldsContainer;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLType;
import graphql.validation.AbstractRule;
import graphql.validation.ErrorFactory;
import graphql.validation.ValidationContext;
import graphql.validation.ValidationErrorCollector;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static graphql.validation.ValidationErrorType.FieldsConflict;

public class OverlappingFieldsCanBeMerged extends AbstractRule {

    final ErrorFactory errorFactory = new ErrorFactory();


    private final List alreadyChecked = new ArrayList<>();

    public OverlappingFieldsCanBeMerged(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) {
        super(validationContext, validationErrorCollector);
    }

    @Override
    public void leaveSelectionSet(SelectionSet selectionSet) {
        Map> fieldMap = new LinkedHashMap<>();
        Set visitedFragmentSpreads = new LinkedHashSet<>();
        collectFields(fieldMap, selectionSet, getValidationContext().getOutputType(), visitedFragmentSpreads);
        List conflicts = findConflicts(fieldMap);
        for (Conflict conflict : conflicts) {
            addError(errorFactory.newError(FieldsConflict, conflict.fields, conflict.reason));
        }

    }


    private List findConflicts(Map> fieldMap) {
        List result = new ArrayList<>();
        for (String name : fieldMap.keySet()) {
            List fieldAndTypes = fieldMap.get(name);
            for (int i = 0; i < fieldAndTypes.size(); i++) {
                for (int j = i + 1; j < fieldAndTypes.size(); j++) {
                    Conflict conflict = findConflict(name, fieldAndTypes.get(i), fieldAndTypes.get(j));
                    if (conflict != null) {
                        result.add(conflict);
                    }
                }
            }
        }
        return result;
    }

    private boolean isAlreadyChecked(Field field1, Field field2) {
        for (FieldPair fieldPair : alreadyChecked) {
            if (fieldPair.field1 == field1 && fieldPair.field2 == field2) {
                return true;
            }
            if (fieldPair.field1 == field2 && fieldPair.field2 == field1) {
                return true;
            }
        }
        return false;
    }

    @SuppressWarnings("ConstantConditions")
    private Conflict findConflict(String responseName, FieldAndType fieldAndType1, FieldAndType fieldAndType2) {

        Field field1 = fieldAndType1.field;
        Field field2 = fieldAndType2.field;


        GraphQLType type1 = fieldAndType1.graphQLType;
        GraphQLType type2 = fieldAndType2.graphQLType;

        String fieldName1 = field1.getName();
        String fieldName2 = field2.getName();


        if (isAlreadyChecked(field1, field2)) {
            return null;
        }
        alreadyChecked.add(new FieldPair(field1, field2));

        // If the statically known parent types could not possibly apply at the same
        // time, then it is safe to permit them to diverge as they will not present
        // any ambiguity by differing.
        // It is known that two parent types could never overlap if they are
        // different Object types. Interface or Union types might overlap - if not
        // in the current state of the schema, then perhaps in some future version,
        // thus may not safely diverge.
        if (!sameType(fieldAndType1.parentType, fieldAndType1.parentType) &&
                fieldAndType1.parentType instanceof GraphQLObjectType &&
                fieldAndType2.parentType instanceof GraphQLObjectType) {
            return null;
        }

        if (!fieldName1.equals(fieldName2)) {
            String reason = String.format("%s: %s and %s are different fields", responseName, fieldName1, fieldName2);
            return new Conflict(responseName, reason, field1, field2);
        }

        if (!sameType(type1, type2)) {
            String name1 = type1 != null ? type1.getName() : "null";
            String name2 = type2 != null ? type2.getName() : "null";
            String reason = String.format("%s: they return differing types %s and %s", responseName, name1, name2);
            return new Conflict(responseName, reason, field1, field2);
        }


        if (!sameArguments(field1.getArguments(), field2.getArguments())) {
            String reason = String.format("%s: they have differing arguments", responseName);
            return new Conflict(responseName, reason, field1, field2);
        }
        SelectionSet selectionSet1 = field1.getSelectionSet();
        SelectionSet selectionSet2 = field2.getSelectionSet();
        if (selectionSet1 != null && selectionSet2 != null) {
            Set visitedFragmentSpreads = new LinkedHashSet<>();
            Map> subFieldMap = new LinkedHashMap<>();
            collectFields(subFieldMap, selectionSet1, type1, visitedFragmentSpreads);
            collectFields(subFieldMap, selectionSet2, type2, visitedFragmentSpreads);
            List subConflicts = findConflicts(subFieldMap);
            if (subConflicts.size() > 0) {
                String reason = String.format("%s: %s", responseName, joinReasons(subConflicts));
                List fields = new ArrayList<>();
                fields.add(field1);
                fields.add(field2);
                fields.addAll(collectFields(subConflicts));
                return new Conflict(responseName, reason, fields);
            }
        }

        return null;

    }

    private List collectFields(List conflicts) {
        List result = new ArrayList<>();
        for (Conflict conflict : conflicts) {
            result.addAll(conflict.fields);
        }
        return result;
    }

    private String joinReasons(List conflicts) {
        StringBuilder result = new StringBuilder();
        result.append("(");
        for (Conflict conflict : conflicts) {
            result.append(conflict.reason);
            result.append(", ");
        }
        result.delete(result.length() - 2, result.length());
        result.append(")");
        return result.toString();
    }

    @SuppressWarnings("SimplifiableIfStatement")
    private boolean sameType(GraphQLType type1, GraphQLType type2) {
        if (type1 == null || type2 == null) return true;
        return type1.equals(type2);
    }

    @SuppressWarnings("SimplifiableIfStatement")
    private boolean sameValue(Value value1, Value value2) {
        if (value1 == null && value2 == null) return true;
        if (value1 == null) return false;
        if (value2 == null) return false;
        return new AstComparator().isEqual(value1, value2);
    }

    private boolean sameArguments(List arguments1, List arguments2) {
        if (arguments1.size() != arguments2.size()) return false;
        for (Argument argument : arguments1) {
            Argument matchedArgument = findArgumentByName(argument.getName(), arguments2);
            if (matchedArgument == null) return false;
            if (!sameValue(argument.getValue(), matchedArgument.getValue())) return false;
        }
        return true;
    }

    private Argument findArgumentByName(String name, List arguments) {
        for (Argument argument : arguments) {
            if (argument.getName().equals(name)) return argument;
        }
        return null;
    }

    private void collectFields(Map> fieldMap, SelectionSet selectionSet, GraphQLType parentType, Set visitedFragmentSpreads) {

        for (Selection selection : selectionSet.getSelections()) {
            if (selection instanceof Field) {
                collectFieldsForField(fieldMap, parentType, (Field) selection);

            } else if (selection instanceof InlineFragment) {
                collectFieldsForInlineFragment(fieldMap, visitedFragmentSpreads, parentType, (InlineFragment) selection);

            } else if (selection instanceof FragmentSpread) {
                collectFieldsForFragmentSpread(fieldMap, visitedFragmentSpreads, (FragmentSpread) selection);
            }
        }

    }

    private void collectFieldsForFragmentSpread(Map> fieldMap, Set visitedFragmentSpreads, FragmentSpread fragmentSpread) {
        FragmentDefinition fragment = getValidationContext().getFragment(fragmentSpread.getName());
        if (fragment == null) return;
        if (visitedFragmentSpreads.contains(fragment.getName())) {
            return;
        }
        visitedFragmentSpreads.add(fragment.getName());
        GraphQLOutputType graphQLType = (GraphQLOutputType) TypeFromAST.getTypeFromAST(getValidationContext().getSchema(),
                fragment.getTypeCondition());
        collectFields(fieldMap, fragment.getSelectionSet(), graphQLType, visitedFragmentSpreads);
    }

    private void collectFieldsForInlineFragment(Map> fieldMap, Set visitedFragmentSpreads, GraphQLType parentType, InlineFragment inlineFragment) {
        GraphQLType graphQLType = inlineFragment.getTypeCondition() != null
                ? (GraphQLOutputType) TypeFromAST.getTypeFromAST(getValidationContext().getSchema(), inlineFragment.getTypeCondition())
                : parentType;
        collectFields(fieldMap, inlineFragment.getSelectionSet(), graphQLType, visitedFragmentSpreads);
    }

    private void collectFieldsForField(Map> fieldMap, GraphQLType parentType, Field field) {
        String responseName = field.getAlias() != null ? field.getAlias() : field.getName();
        if (!fieldMap.containsKey(responseName)) {
            fieldMap.put(responseName, new ArrayList<>());
        }
        GraphQLOutputType fieldType = null;
        if (parentType instanceof GraphQLFieldsContainer) {
            GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) parentType;
            GraphQLFieldDefinition fieldDefinition = getVisibleFieldDefinition(fieldsContainer, field);
            fieldType = fieldDefinition != null ? fieldDefinition.getType() : null;
        }
        fieldMap.get(responseName).add(new FieldAndType(field, fieldType, parentType));
    }

    private GraphQLFieldDefinition getVisibleFieldDefinition(GraphQLFieldsContainer fieldsContainer, Field field) {
        return getValidationContext().getSchema().getFieldVisibility().getFieldDefinition(fieldsContainer, field.getName());
    }

    private static class FieldPair {
        public FieldPair(Field field1, Field field2) {
            this.field1 = field1;
            this.field2 = field2;
        }

        final Field field1;
        final Field field2;

    }

    private static class Conflict {
        final String responseName;
        final String reason;
        final List fields = new ArrayList<>();

        public Conflict(String responseName, String reason, Field field1, Field field2) {
            this.responseName = responseName;
            this.reason = reason;
            this.fields.add(field1);
            this.fields.add(field2);
        }

        public Conflict(String responseName, String reason, List fields) {
            this.responseName = responseName;
            this.reason = reason;
            this.fields.addAll(fields);
        }

    }


    private static class FieldAndType {
        public FieldAndType(Field field, GraphQLType graphQLType, GraphQLType parentType) {
            this.field = field;
            this.graphQLType = graphQLType;
            this.parentType = parentType;
        }

        final Field field;
        final GraphQLType graphQLType;
        final GraphQLType parentType;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy