Получение класса «Enum с параметром» по значению параметра

Частенько в коде встречаются перечисления, дополнительно хранящие некие значения (в виде private final поля обычно).

Чтобы в дальнейшем можно было, например, при сериализации в json это значение подставлять автоматом (@JsonValue у Jackson). Тогда возникает обычно и обратная задача — десериализовать (распарсить) значение обратно в Enum (@JsonCreator у Jackson).

Вот мне и надоело копипастить туда-сюда все эти методы (сериализации/десериализации) между классами Enum. Решил сделать один раз утилитный метод и в проекте им пользоваться. Благо, время позволило.

В принципе, подобный метод есть в недрах Apache Commons EnumUtils, но он работает только со строковым параметром String enumName, плюс выкидывает стандартное исключение. А обычно надо выкинуть некое кастомное, принятое на проекте. Так и родился свой костылёчек, как оно зачастую и бывает.

Получить класс Enum по значению

Импорты копировать не буду, полагаю, IDE предложит их подставить по выбору, если автоматически не сможет этого сделать. Сам утилитный метод примитивен:

public final class EnumUtil {
    private EnumUtil() {}
    public static <T extends ValuedEnum<V>, V> T fromValue(Class<T> enumType, V value) {
        return Arrays.stream(enumType.getEnumConstants())
            .filter(it -> it.getValue().equals(value))
            .findAny()
            .orElseThrow(() -> new IllegalArgumentException(String.format(
                "Wrong value [%s] for enum type [%s]", value, enumType.getSimpleName())
            ));
    }
}

Разве что — в реальных условиях он выбрасывает не IllegalArgumentException, а кастомное исключение.

ValuedEnum — это интерфейс, который реализуют все Enum со значением. Вот он:

public sealed interface ValuedEnum<V> permits
    ComplexValue,
    NumberValue,
    StringValue
{
    V getValue();
    ValuedEnum<V> fromValue(V value);
}

Сделан sealed, чтобы при имплементации (методом копипасты) — был дополнительный «маячок», заставляющий обратить внимание — это именно Enum со значением.

Пример работы и тесты

Набор тестовых Enum

Для демонстрации — три штуки тестовых классов Enum. Значения — Map.Entry, Number и String соответсвенно.

Значение — составной тип

public enum ComplexValue implements ValuedEnum<Entry<String, Number>> {
    COMPLEX_VALUE(Map.entry("One", BigDecimal.ONE));
    private final Entry<String, Number> value;
    ComplexValue(Entry<String, Number> value) {
        this.value = value;
    }
    @Override
    public Entry<String, Number> getValue() {
        return this.value;
    }
    @Override
    public ValuedEnum<Entry<String, Number>> fromValue(Entry<String, Number> value) {
        return EnumUtil.fromValue(ComplexValue.class, value);
    }
}

Значение — число

public enum NumberValue implements ValuedEnum<Number> {
    NUMBER_VALUE(42);
    private final Number value;
    NumberValue(Number value) {
        this.value = value;
    }
    @Override
    public Number getValue() {
        return this.value;
    }
    @Override
    public ValuedEnum<Number> fromValue(Number value) {
        return EnumUtil.fromValue(NumberValue.class, value);
    }
}

Значение — строка (банально)

public enum StringValue implements ValuedEnum<String> {
    STRING_VALUE("String value");
    private final String value;
    StringValue(String value) {
        this.value = value;
    }
    @Override
    public String getValue() {
        return this.value;
    }
    @Override
    public ValuedEnum<String> fromValue(String value) {
        return EnumUtil.fromValue(StringValue.class, value);
    }
}

Все три ничего особенного из себя не представляют, просто демонстрируют, что это работает для различных случаев.

Юнит тесты

Здесь проще привести всё целиком, включая импорты. JUnit пятой версии используется.

import org.assertj.core.api.AssertionsForClassTypes;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.math.BigDecimal;
import java.util.Map;
import java.util.stream.Stream;

@Tag("unit")
class EnumUtilTest {

    private static Stream<Arguments> enumFromValueGoodParams() {
        return Stream.of(
            Arguments.arguments(ComplexValue.class, Map.entry("One", BigDecimal.ONE), ComplexValue.COMPLEX_VALUE),
            Arguments.arguments(NumberValue.class, 42, NumberValue.NUMBER_VALUE),
            Arguments.arguments(StringValue.class, "String value", StringValue.STRING_VALUE)
        );
    }

    private static Stream<Arguments> enumFromValueBadParams() {
        return Stream.of(
            Arguments.arguments(ComplexValue.class, Map.entry("TEN", BigDecimal.TEN)),
            Arguments.arguments(NumberValue.class, 42.42d),
            Arguments.arguments(StringValue.class, "Not found String value")
        );
    }

    @DisplayName("Success parse value to a Enum")
    @ParameterizedTest
    @MethodSource("enumFromValueGoodParams")
    <T extends ValuedEnum<V>, V> void fromValueSuccess(Class<T> enumClass, V enumValue, T expected) {
        var fromValue = EnumUtil.fromValue(enumClass, enumValue);
        Assertions.assertEquals(expected, fromValue);
    }

    @DisplayName("Thrown when parse wrong value to a Enum")
    @ParameterizedTest
    @MethodSource("enumFromValueBadParams")
    <T extends ValuedEnum<V>, V> void fromValueError(Class<T> enumClass, V enumBadValue) {
        AssertionsForClassTypes.assertThatThrownBy(() -> EnumUtil.fromValue(enumClass, enumBadValue))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining(
                String.format("Wrong value [%s] for enum type [%s]", enumBadValue, enumClass.getSimpleName())
            );
    }

}

А результат выполнения этих тестов вынесен в начало заметки — видно, что они проходят 🙂

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *