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);
}
}