GQLField.java

package net.morimekta.providence.graphql.gql;

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

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import java.util.List;
import java.util.Objects;

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

/**
 * Representation of a field in the response JSON. This is called
 * a 'field' as it references a field of data in the JSON response,
 * unrelated to message fields.
 *
 * This may represent a message field or a method call, but not both.
 * And may always have an alias.
 *
 * If the type of the field, or return type of the message is a message
 * or collection of messages, it may have a selection set.
 *
 * {@inheritDoc}
 */
@Immutable
public class GQLField implements GQLSelection {
    @Nullable
    private final String             alias;
    @Nonnull
    private final PField             field;
    @Nullable
    private final PMessage<?>        arguments;
    @Nullable
    private final List<GQLSelection> selectionSet;

    /**
     * Make a simple field selection based on a message field.
     *
     * @param field The field to be represented.
     */
    public GQLField(@Nonnull PField field) {
        this(field, null, null, null);
    }

    /**
     * Make a simple field selection based on a message field.
     *
     * @param field The field to be represented.
     * @param selectionSet SelectionSet for sub-field selection.
     */
    public GQLField(@Nonnull PField field, @Nullable List<GQLSelection> selectionSet) {
        this(field, null, null, selectionSet);
    }

    /**
     * Make a full field selection definition.
     *
     * @param field The field to be represented.
     * @param alias The alias to be used in the JSON output.
     * @param arguments Arguments used for field mutators.
     * @param selectionSet SelectionSet for sub-field selection.
     */
    public GQLField(@Nonnull PField field,
                    @Nullable String alias,
                    @Nullable PMessage<?> arguments,
                    @Nullable List<GQLSelection> selectionSet) {
        this.alias = alias;
        this.field = field;
        this.arguments = arguments;
        if (selectionSet != null && selectionSet.size() > 0) {
            this.selectionSet = UnmodifiableList.copyOf(selectionSet);
        } else {
            this.selectionSet = null;
        }
    }

    /**
     * Check if the input field is represented with this field selection. It will
     * match if it is the same field as this, or if it is the same field as what
     * this field is overriding (from implementing interface).
     * <p>
     * E.g. if 'A' is an interface field, and 'B' and 'C' are the equivalent field
     * in two different messages, and this selection is for 'B', then it is
     * representing 'A', and 'B', but not 'C'. But if this is a selection for 'A',
     * then all three will match.
     *
     * @param inf The field to check.
     * @return If the field is represented by this selection.
     */
    public boolean representing(PField inf) {
        return field.equals(inf) || baseField(field).equals(inf) || field.equals(baseField(inf));
    }

    /**
     * @return The output field alias.
     */
    @Nullable
    public String getAlias() {
        return alias;
    }

    /**
     * @return The message field to be represented.
     */
    @Nonnull
    public PField getField() {
        return field;
    }

    /**
     * @param <M> The argument message type.
     * @return The argument struct. If no arguments or params then null.
     */
    @Nullable
    @SuppressWarnings("unchecked")
    public <M extends PMessage<M>>
    M getArguments() {
        return (M) arguments;
    }

    /**
     * @return The field's selection set, or null if none.
     */
    @Nullable
    public List<GQLSelection> getSelectionSet() {
        return selectionSet;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        if (alias != null) {
            builder.append(alias).append(": ");
        }
        builder.append(field.getName());

        if (arguments != null) {
            builder.append("(");
            boolean first = true;
            for (PField pf : arguments.descriptor().getFields()) {
                if (arguments.has(pf.getId())) {
                    if (!first) {
                        builder.append(", ");
                    } else {
                        first = false;
                    }
                    builder.append(pf.getName())
                           .append(": ")
                           .append(toArgumentString(arguments.get(pf.getId())));
                }
            }
            builder.append(")");
        }
        if (selectionSet != null && selectionSet.size() > 0) {
            if (arguments != null) {
                builder.append(" ");
            }

            builder.append("{");
            boolean first = true;
            for (GQLSelection field : selectionSet) {
                if (!first) {
                    builder.append(", ");
                } else {
                    first = false;
                }
                builder.append(field.toString());
            }
            builder.append("}");
        }
        return builder.toString();
    }

    @Override
    public int hashCode() {
        return Objects.hash(GQLField.class,
                            alias, field, arguments, selectionSet);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof GQLField)) return false;
        GQLField other = (GQLField) o;
        return Objects.equals(alias, other.alias) &&
               Objects.equals(field, other.field) &&
               Objects.equals(arguments, other.arguments) &&
               Objects.equals(selectionSet, other.selectionSet);
    }
}