GQLSelection.java

package net.morimekta.providence.graphql.gql;

import net.morimekta.providence.descriptor.PField;
import net.morimekta.util.collect.UnmodifiableList;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;

import static net.morimekta.providence.graphql.gql.GQLUtil.baseField;

/**
 * When defining a graphql each point in a selection set is called a
 * 'selection'. A selection may be a field or a fragment, each having
 * a distinct definition and uses.
 *
 * Most selections can be recursive, i.e. containing a selection set
 * of it's own. The selection set is ordered, therefore it is returned
 * as a list, not a set.
 */
public interface GQLSelection {
    /**
     * @return List if selections contained within this selection.
     */
    @Nullable
    List<GQLSelection> getSelectionSet();

    /**
     * Check if <b>any</b> of the given fields are in the selection.
     * It will only look the the current struct, but check in all contained
     * and referenced fragments. If only one field is given, this method
     * will return true if and only if {@link #getSelection(PField)} returns
     * a non-empty list.
     *
     * @param fields Fields to check selection for.
     * @return True only if the current selection set contains ony
     *         of the provided fields.
     */
    default boolean hasSelection(@Nonnull PField<?>... fields) {
        // Everything requested.
        if (getSelectionSet() == null) return true;
        if (fields.length == 0) return false;

        for (GQLSelection sel : getSelectionSet()) {
            if (sel instanceof GQLField) {
                GQLField sf = (GQLField) sel;
                for (PField<?> field : fields) {
                    if (sf.representing(field)) {
                        return true;
                    }
                }
            } else if (sel instanceof GQLFragment) {
                GQLFragment fragment = (GQLFragment) sel;
                if (fragment.hasSelection(fields)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Browse downward fields so that we figure out if the selection (based on
     * a field path based on the returning structure) if selected. E.g.
     * <code>hasSelectionPath("a", "b")</code> will first look for the selection
     * of "a", then for selection of "b" within a. Will treat a non-selection
     * as the selection being there.
     *
     * @param fields Fields as in a field path to check from the current
     *               selection.
     * @return If the selection path is selected or on per default.
     */
    default boolean hasSelectionPath(@Nonnull PField<?>... fields) {
        if (fields.length == 0) {
            throw new IllegalArgumentException("No fields to check selection for");
        }
        if (fields.length == 1) {
            return hasSelection(fields[0]);
        }

        // Part of default selection. Everything is selected...
        if (getSelectionSet() == null) {
            return true;
        }
        PField<?> field = fields[0];
        PField<?>[] args = new PField[fields.length - 1];

        System.arraycopy(fields, 1, args, 0, args.length);

        for (GQLSelection sel : getSelection(field)) {
            // Get field selection for the requested field.
            if (sel.hasSelectionPath(args)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get all selection entries related to the given field. It will
     * only look at the current struct, but look at all levels of
     * fragments.
     *
     * The result will contain one entry for each time the field is
     * requested, so if multiple fragments requests the same field, this
     * will return one entry per reference. If it contains more than one,
     * the query <b>should</b> only have one such without alias, and all
     * others <b>should</b> use unique aliases, but is not required.
     *
     * This method will return a non-empty list if and only if
     * {@link #hasSelection(PField[])} called with the same field returns
     * true.
     *
     * @param field The field to get selection for.
     * @return List of selection related to the field.
     */
    @Nonnull
    default List<GQLSelection> getSelection(@Nonnull PField<?> field) {
        if (getSelectionSet() == null) return UnmodifiableList.listOf();
        field = baseField(field);

        List<GQLSelection> selection = new ArrayList<>();
        for (GQLSelection sel : getSelectionSet()) {
            if (sel instanceof GQLField) {
                GQLField sf = (GQLField) sel;
                if (sf.representing(field)) {
                    selection.add(sf);
                }
            } else if (sel instanceof GQLFragment) {
                GQLFragment fragment = (GQLFragment) sel;
                selection.addAll(fragment.getSelection(field));
            }
        }

        return UnmodifiableList.copyOf(selection);
    }
}