meyfa/structogram2byob

View on GitHub
src/main/java/structogram2byob/blocks/BlockDescription.java

Summary

Maintainability
A
2 hrs
Test Coverage
A
97%
package structogram2byob.blocks;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import structogram2byob.ScratchType;


/**
 * A "block description" is the user-facing part of a block, i.e. the labels and
 * parameter acceptors making up its visual format.
 */
public class BlockDescription
{
    private final List<Object> parts;

    /**
     * @param parts The parts this description consists of.
     */
    private BlockDescription(List<Object> parts)
    {
        this.parts = Collections.unmodifiableList(new ArrayList<>(parts));
    }

    /**
     * @return The number of separate parts this description has.
     */
    public int countParts()
    {
        return parts.size();
    }

    /**
     * Checks whether the part at the given index is a parameter, as opposed to
     * a plain label.
     *
     * @param index The part index.
     * @return Whether the part is a parameter.
     */
    public boolean isParameter(int index)
    {
        return parts.get(index) instanceof Parameter;
    }

    /**
     * Obtains the label at the given index. Note that depending on the block
     * description, parameters can be labeled as well.
     *
     * @param index The part index.
     * @return The part's label.
     */
    public String getLabel(int index)
    {
        if (isParameter(index)) {
            return ((Parameter) parts.get(index)).label;
        }
        return (String) parts.get(index);
    }

    /**
     * Obtains the type of the parameter part at the given index.
     *
     * @param index The part index.
     * @return The parameter type.
     *
     * @throws IllegalArgumentException If the part is not a parameter.
     */
    public ScratchType getType(int index)
    {
        if (!isParameter(index)) {
            throw new IllegalArgumentException("tried accessing label as parameter");
        }
        return ((Parameter) parts.get(index)).type;
    }

    /**
     * Checks whether the parameter part at the given index accepts a variable
     * number of values.
     *
     * @param index The part index.
     * @return Whether the parameter accepts a variable number of values.
     *
     * @throws IllegalArgumentException If the part is not a parameter.
     */
    public boolean isList(int index)
    {
        if (!isParameter(index)) {
            throw new IllegalArgumentException("tried accessing label as parameter");
        }
        return ((Parameter) parts.get(index)).list;
    }

    /**
     * Checks whether this description is compatible to the given one in the
     * sense that all the labels match, and all parameter types are compatible
     * (via {@link ScratchType#isAssignableFrom(ScratchType)}).
     *
     * @param o The block description to check assignability for.
     * @return Whether the description is compatible with this one.
     */
    public boolean isAssignableFrom(BlockDescription o)
    {
        final int iMax = countParts(), jMax = o.countParts();
        int i, j;

        for (i = 0, j = i; i < iMax && j < jMax; ++i) {

            if (isParameter(i)) {
                ScratchType paramType = getType(i);

                if (isList(i)) {
                    // consume all params that match
                    while (j < jMax && o.isParameter(j) && paramType.isAssignableFrom(o.getType(j))) {
                        ++j;
                    }
                    continue;
                }

                // check param types compatible
                if (!o.isParameter(j) || !paramType.isAssignableFrom(o.getType(j))) {
                    return false;
                }
            } else if (o.isParameter(j) || !getLabel(i).equals(o.getLabel(j))) {
                // labels must match for non-parameters
                return false;
            }

            ++j;
        }

        // verify that both descriptions were iterated over completely
        return i == iMax && j == jMax;
    }

    /**
     * Converts this description into a "user spec" as employed by custom blocks
     * (e.g. {@code "foo (bar) baz"} becomes {@code "foo %bar baz"}).
     *
     * @return This description as a user spec.
     */
    public String toUserSpec()
    {
        return parts.stream().map(p -> {
            if (p instanceof Parameter) {
                return "%" + ((Parameter) p).label;
            }
            return (String) p;
        }).collect(Collectors.joining(" "));
    }

    /**
     * Converts this description back to a builder for further editing. Changes
     * to the builder do not write back to this instance.
     *
     * @return An editable builder containing all parts of this description.
     */
    public Builder toBuilder()
    {
        Builder b = new Builder();
        b.parts.addAll(parts);

        return b;
    }

    @Override
    public int hashCode()
    {
        return Objects.hashCode(parts);
    }

    @Override
    public boolean equals(Object obj)
    {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        BlockDescription other = (BlockDescription) obj;
        return Objects.equals(parts, other.parts);
    }

    @Override
    public String toString()
    {
        return parts.stream().map(Object::toString).collect(Collectors.joining(" "));
    }

    /**
     * Builder class for convenient construction of block descriptions.
     */
    public static class Builder
    {
        private final List<Object> parts = new ArrayList<>();

        /**
         * Adds a parameter part of the given type to the object being built.
         *
         * @param type The value type accepted by the parameter.
         * @return This instance.
         */
        public Builder param(ScratchType type)
        {
            parts.add(new Parameter(type, false, null));
            return this;
        }

        /**
         * Adds a labeled parameter part of the given type to the object being
         * built.
         *
         * @param type The value type accepted by the parameter.
         * @param label The parameter's label.
         * @return This instance.
         */
        public Builder param(ScratchType type, String label)
        {
            parts.add(new Parameter(type, false, label));
            return this;
        }

        /**
         * Adds a parameter part accepting a variable number of values of the
         * given type to the object being built.
         *
         * @param type The value type accepted by the parameter.
         * @return This instance.
         */
        public Builder paramList(ScratchType type)
        {
            parts.add(new Parameter(type, true, null));
            return this;
        }

        /**
         * Adds a labeled parameter part accepting a variable number of values
         * of the given type to the object being built.
         *
         * @param type The value type accepted by the parameter.
         * @param label The parameter's label.
         * @return This instance.
         */
        public Builder paramList(ScratchType type, String label)
        {
            parts.add(new Parameter(type, true, label));
            return this;
        }

        /**
         * Adds a label part to the object being built.
         *
         * @param label The label.
         * @return This instance.
         */
        public Builder label(String label)
        {
            parts.add(label);
            return this;
        }

        /**
         * Constructs a block description from the parts added to this builder.
         *
         * @return A block description.
         */
        public BlockDescription build()
        {
            return new BlockDescription(parts);
        }
    }

    /**
     * Class used in block descriptions for specifying parameters.
     */
    private static class Parameter
    {
        private final ScratchType type;
        private final boolean list;
        private final String label;

        /**
         * Constructs a new parameter that accepts the given type, may be a
         * list, and has the given label.
         *
         * @param type The type of value that is accepted.
         * @param list Whether the param accepts a variable number of values.
         * @param label The parameter's label. May be null if there is no label.
         */
        public Parameter(ScratchType type, boolean list, String label)
        {
            this.type = type;
            this.list = list;
            this.label = label;
        }

        @Override
        public int hashCode()
        {
            return Objects.hash(label, list, type);
        }

        @Override
        public boolean equals(Object obj)
        {
            if (this == obj) {
                return true;
            }
            if (obj == null || getClass() != obj.getClass()) {
                return false;
            }
            Parameter other = (Parameter) obj;
            return Objects.equals(label, other.label) && list == other.list && type == other.type;
        }

        @Override
        public String toString()
        {
            StringBuilder sb = new StringBuilder();

            sb.append('(');
            sb.append(type.name().toLowerCase());
            if (list) {
                sb.append("...");
            }
            sb.append(')');

            return sb.toString();
        }
    }
}