TypeReference.java

package net.morimekta.providence.types;

import net.morimekta.providence.descriptor.PPrimitive;
import net.morimekta.util.Strings;
import net.morimekta.util.collect.SetOperations;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static net.morimekta.util.collect.UnmodifiableSet.copyOf;
import static net.morimekta.util.collect.UnmodifiableSet.setOf;

/**
 * Simple class to represent a type reference or declaration reference
 * (e.g. for constant, typedef etc).
 */
public final class TypeReference {
    private static final String      GLOBAL            = "";
    private static final String      LIST              = "list";
    private static final String      SET               = "set";
    private static final String      MAP               = "map";
    private static final String      IDENTIFIER        = "[_a-zA-Z][_a-zA-Z0-9]*";
    private static final String      TYPENAME          = IDENTIFIER + "(\\." + IDENTIFIER + "\\.(request|response))?";
    private static final Set<String> ILLEGAL_TYPE = setOf(
            "struct", "union", "exception", "interface", "const", "enum",
            "namespace", "include", "implements", "of",
            "service", "extends", "throws");
    private static final Set<String> NATIVE_TYPE = SetOperations.union(
            setOf(SET, LIST, MAP),
            setOf("i8"),
            copyOf(Arrays.stream(PPrimitive.values())
                         .map(PPrimitive::getName)
                         .collect(Collectors.toSet())));

    public final String        programName;
    public final String        typeName;
    public final TypeReference keyType;
    public final TypeReference valueType;

    TypeReference(@Nonnull String programName,
                  @Nonnull String typeName,
                  @Nullable TypeReference keyType,
                  @Nullable TypeReference valueType) {
        this.programName = programName;
        this.typeName = typeName;
        this.keyType = keyType;
        this.valueType = valueType;

        if (!programName.equals(GLOBAL) && !programName.matches(IDENTIFIER)) {
            throw new IllegalArgumentException("Bad program name '" + programName + "'");
        } else if (typeName.isEmpty()) {
            throw new IllegalArgumentException("Empty type name");
        } else if (!typeName.matches(TYPENAME)) {
            throw new IllegalArgumentException("Bad type '" + typeName + "'");
        }

        if (isMap()) {
            if(keyType == null || valueType == null) {
                throw new IllegalArgumentException("Map without key or value type");
            }
        } else if (isSet()) {
            if (valueType == null) {
                throw new IllegalArgumentException("Set without value type");
            }
        } else if (isList()) {
            if (valueType == null) {
                throw new IllegalArgumentException("List without value type");
            }
        } else if (ILLEGAL_TYPE.contains(typeName)) {
            throw new IllegalArgumentException("Not allowed type name: '" + typeName + "'");
        } else if (valueType != null || keyType != null) {
            throw new IllegalArgumentException("Non-container with key or value type");
        }
    }

    public boolean isNativeType() {
        return NATIVE_TYPE.contains(typeName);
    }

    public boolean isContainer() {
        return isList() || isSet() || isMap();
    }

    public boolean isList() {
        return LIST.equals(typeName);
    }

    public boolean isSet() {
        return SET.equals(typeName);
    }

    public boolean isMap() {
        return MAP.equals(typeName);
    }

    /**
     * Create a simple type reference. This can only contain simple (program, type)
     * references. The type name can contain '.', but only for service request and response
     * types.
     *
     * @param programContext The local program context.
     * @param typeName The type name.
     * @return The type reference.
     */
    @Nonnull
    public static TypeReference ref(@Nonnull String programContext, @Nonnull String typeName) {
        return new TypeReference(programContext, typeName, null, null);
    }

    /**
     * Create a type reference from a global reference name. This must include
     * a program name and a type name, and nothing else.
     *
     * @param globalName The global type reference string.
     * @return The type reference.
     */
    @Nonnull
    public static TypeReference parseType(@Nonnull String globalName) {
        if (globalName.isEmpty()) {
            throw new IllegalArgumentException("Empty type name");
        }
        try {
            return parseInternal(GLOBAL, globalName);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException(e.getMessage() + " in reference '" + globalName + "'", e);
        }
    }

    /**
     * Create a type reference from a program context. If the type name
     * contains it's own program name (program.Type) then that program
     * name is used, otherwise the local program context is used.
     *
     * @param programContext The local program context.
     * @param typeName The type name.
     * @return The type reference.
     */
    @Nonnull
    public static TypeReference parseType(@Nonnull String programContext, @Nonnull String typeName) {
        if (typeName.isEmpty()) {
            throw new IllegalArgumentException("Empty type");
        }
        if (programContext.isEmpty()) {
            throw new IllegalArgumentException("Empty program name");
        }
        if (!programContext.matches(IDENTIFIER)) {
            throw new IllegalArgumentException("Bad program name '" + programContext + "'");
        }
        try {
            return parseInternal(programContext, typeName);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException(e.getMessage() + " in reference '" + typeName + "' in program '" + programContext + "'", e);
        }
    }

    @Nonnull
    private static TypeReference parseInternal(@Nonnull String programContext, @Nonnull String typeName) {
        if (typeName.startsWith("list<") && typeName.endsWith(">")) {
            String valueType = typeName.substring(5, typeName.length() - 1);
            if (valueType.isEmpty()) {
                throw new IllegalArgumentException("Empty value type");
            }
            return new TypeReference(GLOBAL, LIST, null,
                                     parseInternal(programContext, valueType));
        } else if (typeName.startsWith("set<") && typeName.endsWith(">")) {
            String valueType = typeName.substring(4, typeName.length() - 1);
            if (valueType.isEmpty()) {
                throw new IllegalArgumentException("Empty value type");
            }
            return new TypeReference(GLOBAL, SET, null,
                                     parseInternal(programContext, valueType));
        } else if (typeName.startsWith("map<") && typeName.endsWith(">")) {
            String generics = typeName.substring(4, typeName.length() - 1);
            if (generics.isEmpty()) {
                throw new IllegalArgumentException("Empty key and value type");
            }
            String[] parts = generics.split(",", 2);
            if (parts.length != 2) {
                throw new IllegalArgumentException("Map without value type: '" + typeName + "'");
            }
            if (parts[0].isEmpty()) {
                throw new IllegalArgumentException("Empty key type");
            }
            if (parts[1].isEmpty()) {
                throw new IllegalArgumentException("Empty value type");
            }
            return new TypeReference(GLOBAL, MAP,
                                     parseInternal(programContext, parts[0]),
                                     parseInternal(programContext, parts[1]));
        } else if (NATIVE_TYPE.contains(typeName)) {
            return new TypeReference(GLOBAL, typeName, null, null);
        }

        //                         name -> context.name
        //                 program.name -> program.name
        //         service.name.request -> context.service.name.request
        // program.service.name.request -> program.service.name.request

        String[] parts = typeName.split("\\.", Byte.MAX_VALUE);
        if (parts.length == 1) {
            if (programContext.equals(GLOBAL)) {
                throw new IllegalArgumentException("No program for type '" + typeName + "'");
            }
            return new TypeReference(programContext, typeName, null, null);
        } else if (parts.length == 2) {
            if (parts[0].equals(GLOBAL)) {
                throw new IllegalArgumentException("Empty program for name '" + typeName + "'");
            }
            return new TypeReference(parts[0], parts[1], null, null);
        } else if (parts.length == 3) {
            if (programContext.equals(GLOBAL)) {
                throw new IllegalArgumentException("No program for type '" + typeName + "'");
            }
            return new TypeReference(programContext, typeName, null, null);
        } else if (parts.length == 4) {
            if (parts[0].equals(GLOBAL)) {
                throw new IllegalArgumentException("Empty program for name '" + typeName + "'");
            }
            typeName = Strings.join(".", (Object[]) Arrays.copyOfRange(parts, 1, parts.length));
            return new TypeReference(parts[0], typeName, null, null);
        }
        throw new IllegalArgumentException("Bad type reference: '" + typeName + "'");
    }

    @Override
    public String toString() {
        if (typeName.equals(LIST) || typeName.equals(SET)) {
            return typeName + "<" + valueType + ">";
        } else if (typeName.equals(MAP)) {
            return typeName + "<" + keyType + "," + valueType + ">";
        } else if (NATIVE_TYPE.contains(typeName)) {
            return typeName;
        }

        return programName + "." + typeName;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof TypeReference)) return false;
        TypeReference other = (TypeReference) obj;
        return Objects.equals(programName, other.programName) &&
               Objects.equals(typeName, other.typeName) &&
               Objects.equals(keyType, other.keyType) &&
               Objects.equals(valueType, other.valueType);
    }

    @Override
    public int hashCode() {
        return Objects.hash(getClass(), programName, typeName);
    }
}