Skip to content

Commit

Permalink
Merge pull request #501 from SpineEventEngine/improve-enum-types-conv…
Browse files Browse the repository at this point in the history
…ersion

Improve enum types conversion
  • Loading branch information
dmitrykuzmin authored Dec 17, 2019
2 parents 0ed9e68 + 3f1ccd9 commit f0b2254
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 132 deletions.
72 changes: 63 additions & 9 deletions base/src/main/java/io/spine/protobuf/TypeConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,17 @@
import com.google.protobuf.Int32Value;
import com.google.protobuf.Int64Value;
import com.google.protobuf.Message;
import com.google.protobuf.ProtocolMessageEnum;
import com.google.protobuf.StringValue;
import com.google.protobuf.UInt32Value;
import com.google.protobuf.UInt64Value;
import io.spine.annotation.Internal;
import io.spine.type.TypeUrl;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.spine.protobuf.AnyPacker.unpack;
import static io.spine.util.Exceptions.newIllegalArgumentException;

/**
* A utility for converting the {@linkplain Message Protobuf Messages} (in form of {@link Any}) into
Expand All @@ -57,12 +60,14 @@
* the official document</a>.
* <li>{@linkplain Enum Java Enum} types - the passed {@link Any} is unpacked into the {@link
* EnumValue} type and then is converted to the Java Enum through the value {@linkplain
* EnumValue#getName() name}.
* EnumValue#getName() name} or {@linkplain EnumValue#getNumber() number}.
* </ul>
*/
@Internal
public final class TypeConverter {

private static final TypeUrl ENUM_VALUE_TYPE_URL = TypeUrl.of(EnumValue.class);

/** Prevents instantiation of this utility class. */
private TypeConverter() {
}
Expand All @@ -78,6 +83,7 @@ private TypeConverter() {
public static <T> T toObject(Any message, Class<T> target) {
checkNotNull(message);
checkNotNull(target);
checkNotRawEnum(message, target);
MessageCaster<? super Message, T> caster = MessageCaster.forType(target);
Message genericMessage = unpack(message);
T result = caster.convert(genericMessage);
Expand Down Expand Up @@ -131,6 +137,25 @@ public static <T, M extends Message> M toMessage(T value, Class<M> messageClass)
return messageClass.cast(message);
}

/**
* Makes sure no incorrectly packed enum values are passed to the message caster.
*
* <p>Currently, the enum values can only be converted from the {@link EnumValue} proto type.
* All other enum representations, including plain strings and numbers, are not supported.
*/
private static void checkNotRawEnum(Any message, Class<?> target) {
if (!target.isEnum()) {
return;
}
String typeUrl = message.getTypeUrl();
String enumValueTypeUrl = ENUM_VALUE_TYPE_URL.value();
checkArgument(
enumValueTypeUrl.equals(typeUrl),
"Currently the conversion of enum types packed as `%s` is not supported. " +
"Please make sure the enum value is wrapped with `%s` on the calling site.",
typeUrl, enumValueTypeUrl);
}

/**
* The {@link Function} performing the described type conversion.
*/
Expand All @@ -145,7 +170,7 @@ private static <M extends Message, T> MessageCaster<M, T> forType(Class<T> cls)
caster = new BytesCaster();
} else if (Enum.class.isAssignableFrom(cls)) {
@SuppressWarnings("unchecked") // Checked at runtime.
Class<? extends Enum> enumCls = (Class<? extends Enum>) cls;
Class<? extends Enum<?>> enumCls = (Class<? extends Enum<?>>) cls;
caster = new EnumCaster(enumCls);
} else {
caster = new PrimitiveTypeCaster<>();
Expand Down Expand Up @@ -188,32 +213,61 @@ protected BytesValue toMessage(ByteString input) {
}
}

private static final class EnumCaster extends MessageCaster<EnumValue, Enum> {
private static final class EnumCaster extends MessageCaster<EnumValue, Enum<?>> {

@SuppressWarnings("rawtypes") // Needed to be able to pass the value to `Enum.valueOf(...)`.
private final Class<? extends Enum> type;

EnumCaster(Class<? extends Enum> type) {
EnumCaster(Class<? extends Enum<?>> type) {
super();
this.type = type;
}

@Override
protected Enum toObject(EnumValue input) {
protected Enum<?> toObject(EnumValue input) {
String name = input.getName();
@SuppressWarnings("unchecked") // Checked at runtime.
Enum value = Enum.valueOf(type, name);
return value;
if (name.isEmpty()) {
int number = input.getNumber();
return convertByNumber(number);
} else {
return convertByName(name);
}
}

private Enum<?> convertByNumber(int number) {
Enum<?>[] constants = type.getEnumConstants();
for (Enum<?> constant : constants) {
ProtocolMessageEnum asProtoEnum = (ProtocolMessageEnum) constant;
int valueNumber = asProtoEnum.getNumber();
if (number == valueNumber) {
return constant;
}
}
throw unknownNumber(number);
}

@SuppressWarnings("unchecked") // Checked at runtime.
private Enum<?> convertByName(String name) {
return Enum.valueOf(type, name);
}

@Override
protected EnumValue toMessage(Enum input) {
protected EnumValue toMessage(Enum<?> input) {
String name = input.name();
ProtocolMessageEnum asProtoEnum = (ProtocolMessageEnum) input;
EnumValue value = EnumValue
.newBuilder()
.setName(name)
.setNumber(asProtoEnum.getNumber())
.build();
return value;
}

private IllegalArgumentException unknownNumber(int number) {
throw newIllegalArgumentException(
"Could not find a enum value of type `%s` for number `%d`.",
type.getCanonicalName(), number);
}
}

private static final class MessageTypeCaster extends MessageCaster<Message, Message> {
Expand Down
148 changes: 118 additions & 30 deletions base/src/test/java/io/spine/protobuf/TypeConverterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,21 @@
import com.google.protobuf.StringValue;
import com.google.protobuf.UInt32Value;
import com.google.protobuf.UInt64Value;
import io.spine.test.protobuf.TaskStatus;
import io.spine.testing.UtilityClassTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static io.spine.base.Identifier.newUuid;
import static io.spine.protobuf.TypeConverter.toMessage;
import static io.spine.protobuf.given.TypeConverterTestEnv.TaskStatus.SUCCESS;
import static io.spine.test.protobuf.TaskStatus.EXECUTING;
import static io.spine.test.protobuf.TaskStatus.FAILED;
import static io.spine.test.protobuf.TaskStatus.SUCCESS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@DisplayName("TypeConverter utility class should")
@DisplayName("`TypeConverter` utility class should")
class TypeConverterTest extends UtilityClassTest<TypeConverter> {

TypeConverterTest() {
Expand All @@ -64,79 +68,70 @@ class Map {

@Test
@DisplayName("arbitrary message to itself")
void map_arbitrary_message_to_itself() {
void arbitraryMessageToItself() {
Message message = StringValue.of(newUuid());
checkMapping(message, message);
}

@Test
@DisplayName("Int32Value to int")
void map_Int32Value_to_int() {
@DisplayName("`Int32Value` to `int`")
void int32ValueToInt() {
int rawValue = 42;
Message value = Int32Value.of(rawValue);
checkMapping(rawValue, value);
}

@Test
@DisplayName("Int64Value to int")
void map_Int64Value_to_long() {
@DisplayName("`Int64Value` to `int`")
void int64ValueToLong() {
long rawValue = 42;
Message value = Int64Value.of(rawValue);
checkMapping(rawValue, value);
}

@Test
@DisplayName("FloatValue to float")
void map_FloatValue_to_float() {
@DisplayName("`FloatValue` to `float`")
void floatValueToFloat() {
float rawValue = 42.0f;
Message value = FloatValue.of(rawValue);
checkMapping(rawValue, value);
}

@Test
@DisplayName("DoubleValue to double")
void map_DoubleValue_to_double() {
@DisplayName("`DoubleValue` to `double`")
void doubleValueToDouble() {
double rawValue = 42.0;
Message value = DoubleValue.of(rawValue);
checkMapping(rawValue, value);
}

@Test
@DisplayName("BoolValue to boolean")
void map_BoolValue_to_boolean() {
@DisplayName("`BoolValue` to `boolean`")
void boolValueToBoolean() {
boolean rawValue = true;
Message value = BoolValue.of(rawValue);
checkMapping(rawValue, value);
}

@Test
@DisplayName("StringValue to String")
void map_StringValue_to_String() {
@DisplayName("`StringValue` to `String`")
void stringValueToString() {
String rawValue = "Hello";
Message value = StringValue.of(rawValue);
checkMapping(rawValue, value);
}

@Test
@DisplayName("BytesValue to ByteString")
void map_BytesValue_to_ByteString() {
@DisplayName("`BytesValue` to `ByteString`")
void bytesValueToByteString() {
ByteString rawValue = ByteString.copyFrom("Hello!", Charsets.UTF_8);
Message value = BytesValue.of(rawValue);
checkMapping(rawValue, value);
}

@Test
@DisplayName("EnumValue to Enum")
void map_EnumValue_to_Enum() {
Message value = EnumValue.newBuilder()
.setName(SUCCESS.name())
.build();
checkMapping(SUCCESS, value);
}

@Test
@DisplayName("UInt32 to int")
void map_uint32_to_int() {
@DisplayName("`UInt32` to `int`")
void uint32ToInt() {
int value = 42;
UInt32Value wrapped = UInt32Value.of(value);
Any packed = AnyPacker.pack(wrapped);
Expand All @@ -145,8 +140,8 @@ void map_uint32_to_int() {
}

@Test
@DisplayName("UInt64 to int")
void map_uint64_to_long() {
@DisplayName("`UInt64` to `int`")
void uint64ToLong() {
long value = 42L;
UInt64Value wrapped = UInt64Value.of(value);
Any packed = AnyPacker.pack(wrapped);
Expand All @@ -165,6 +160,99 @@ private void checkMapping(Object javaObject,
}
}

@Nested
@DisplayName("convert `EnumValue` to `Enum`")
class ConvertEnumValueToEnum {

@Test
@DisplayName("if the `EnumValue` has a constant name specified")
void ifHasName() {
EnumValue value = EnumValue.newBuilder()
.setName(SUCCESS.name())
.build();
checkConverts(value, SUCCESS);
}

@Test
@DisplayName("if the `EnumValue` has a constant number specified")
void ifHasNumber() {
EnumValue value = EnumValue.newBuilder()
.setNumber(EXECUTING.getNumber())
.build();
checkConverts(value, EXECUTING);
}

@Test
@DisplayName("using the constant name if both the name and the number are specified")
void preferringConversionWithName() {
// Set the different name and number just for the sake of test.
EnumValue value = EnumValue.newBuilder()
.setName(SUCCESS.name())
.setNumber(FAILED.getNumber())
.build();
checkConverts(value, SUCCESS);
}

private void checkConverts(EnumValue enumValue, Enum<?> expected) {
Any wrapped = AnyPacker.pack(enumValue);
Object mappedJavaObject =
TypeConverter.toObject(wrapped, expected.getDeclaringClass());
assertEquals(expected, mappedJavaObject);
}
}

@SuppressWarnings("CheckReturnValue") // The method is called to throw exception.
@Test
@DisplayName("throw an `IAE` when the `EnumValue` with an unknown name is passed")
void throwOnUnknownName() {
String unknownName = "some_name";
EnumValue value = EnumValue.newBuilder()
.setName(unknownName)
.build();
Any wrapped = AnyPacker.pack(value);
assertThrows(IllegalArgumentException.class,
() -> TypeConverter.toObject(wrapped, TaskStatus.class));
}

@SuppressWarnings("CheckReturnValue") // The method is called to throw exception.
@Test
@DisplayName("throw an `IAE` when the `EnumValue` with an unknown number is passed")
void throwOnUnknownNumber() {
int unknownValue = 156;
EnumValue value = EnumValue.newBuilder()
.setNumber(unknownValue)
.build();
Any wrapped = AnyPacker.pack(value);
assertThrows(IllegalArgumentException.class,
() -> TypeConverter.toObject(wrapped, TaskStatus.class));
}

@SuppressWarnings("CheckReturnValue") // The method is called to throw exception.
@Test
@DisplayName("throw an `IAE` when converting a non-`EnumValue` object to a `Enum`")
void throwOnRawValuesForEnum() {
Int32Value enumNumber = Int32Value
.newBuilder()
.setValue(SUCCESS.getNumber())
.build();
Any packed = AnyPacker.pack(enumNumber);
assertThrows(IllegalArgumentException.class,
() -> TypeConverter.toObject(packed, TaskStatus.class));
}

@Test
@DisplayName("convert `Enum` to `EnumValue`")
void convertEnumToEnumValue() {
Any restoredWrapped = TypeConverter.toAny(SUCCESS);
Message restored = AnyPacker.unpack(restoredWrapped);
EnumValue expected = EnumValue
.newBuilder()
.setName(SUCCESS.name())
.setNumber(SUCCESS.getNumber())
.build();
assertEquals(expected, restored);
}

@Nested
@DisplayName("convert")
class Convert {
Expand Down
Loading

0 comments on commit f0b2254

Please sign in to comment.