MonthDeserializerTest.java

package tools.jackson.databind.ext.javatime.deser;

import java.time.Month;
import java.time.temporal.TemporalAccessor;

import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;

import tools.jackson.databind.MapperFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectReader;
import tools.jackson.databind.cfg.CoercionAction;
import tools.jackson.databind.cfg.CoercionInputShape;
import tools.jackson.databind.cfg.DateTimeFeature;
import tools.jackson.databind.exc.InvalidFormatException;
import tools.jackson.databind.exc.MismatchedInputException;
import tools.jackson.databind.ext.javatime.DateTimeTestBase;
import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
import tools.jackson.databind.json.JsonMapper;

import static org.junit.jupiter.api.Assertions.*;

public class MonthDeserializerTest extends DateTimeTestBase
{
    static class Wrapper {
        public Month value;

        public Wrapper(Month v) { value = v; }
        public Wrapper() { }
    }

    @ParameterizedTest
    @EnumSource(Month.class)
    public void testDeserializationAsString01_oneBased(Month expectedMonth) throws Exception
    {
        int monthNum = expectedMonth.getValue();
        assertEquals(expectedMonth, readerForOneBased().readValue("\"" + monthNum + '"'));
    }

    @ParameterizedTest
    @EnumSource(Month.class)
    public void testDeserializationAsString01_zeroBased(Month expectedMonth) throws Exception
    {
        int monthNum = expectedMonth.ordinal();
        assertEquals(expectedMonth, readerForZeroBased().readValue("\"" + monthNum + '"'));
    }


    @ParameterizedTest
    @EnumSource(Month.class)
    public void testDeserializationAsString02_oneBased(Month month) throws Exception
    {
        assertEquals(month, readerForOneBased().readValue("\"" + month.name() + '"'));
    }

    @ParameterizedTest
    @EnumSource(Month.class)
    public void testDeserializationAsString02_zeroBased(Month month) throws Exception
    {
        assertEquals(month, readerForOneBased().readValue("\"" + month.name() + '"'));
    }

    @ParameterizedTest
    @CsvSource({
            "notamonth , 'Cannot deserialize value of type `java.time.Month` from String \"notamonth\": not one of the values accepted for Enum class:'",
            "JANUAR    , 'Cannot deserialize value of type `java.time.Month` from String \"JANUAR\": not one of the values accepted for Enum class:'",
            "march     , 'Cannot deserialize value of type `java.time.Month` from String \"march\": not one of the values accepted for Enum class:'",
            "0         , 'Month number 0 not allowed for 1-based Month.'",
            "13        , 'Month number 13 not allowed for 1-based Month.'",
    })
    public void testBadDeserializationAsString01_oneBased(String monthSpec, String expectedMessage) {
        String value = "\"" + monthSpec + '"';
        assertError(
            () -> readerForOneBased().readValue(value),
            InvalidFormatException.class,
            expectedMessage
        );
    }

    static void assertError(Executable codeToRun, Class<? extends Throwable> expectedException, String expectedMessage) {
        try {
            codeToRun.execute();
            fail(String.format("Expecting %s, but nothing was thrown!", expectedException.getName()));
        } catch (Throwable actualException) {
            if (!expectedException.isInstance(actualException)) {
                fail(String.format("Expecting exception of type %s, but %s was thrown instead", expectedException.getName(), actualException.getClass().getName()));
            }
            if (actualException.getMessage() == null || !actualException.getMessage().contains(expectedMessage)) {
                fail(String.format("Expecting exception with message containing: '%s', but the actual error message was:'%s'", expectedMessage, actualException.getMessage()));
            }
        }
    }

    private final ObjectMapper MAPPER = newMapper();

    @Test
    public void testDeserialization01_zeroBased() throws Exception
    {
        assertEquals(Month.FEBRUARY, readerForZeroBased().readValue("1"));
    }

    @Test
    public void testDeserialization01_oneBased() throws Exception
    {
        assertEquals(Month.JANUARY, readerForOneBased().readValue("1"));
    }

    @Test
    public void testDeserialization02_zeroBased() throws Exception
    {
        assertEquals(Month.SEPTEMBER, readerForZeroBased().readValue("\"8\""));
    }

    @Test
    public void testDeserialization02_oneBased() throws Exception
    {
        assertEquals(Month.AUGUST, readerForOneBased().readValue("\"8\""));
    }

    @Test
    public void testDeserializationWithTypeInfo01_oneBased() throws Exception
    {
        ObjectMapper MAPPER = JsonMapper.builder()
            .addMixIn(TemporalAccessor.class, MockObjectConfiguration.class)
            .enable(DateTimeFeature.ONE_BASED_MONTHS)
            .build();

        TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",11]", TemporalAccessor.class);
        assertEquals(Month.NOVEMBER, value);
    }

    @Test
    public void testDeserializationWithTypeInfo01_zeroBased() throws Exception
    {
        ObjectMapper MAPPER = JsonMapper.builder()
                .addMixIn(TemporalAccessor.class, MockObjectConfiguration.class)
                .disable(DateTimeFeature.ONE_BASED_MONTHS)
                .build();

        TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",\"11\"]", TemporalAccessor.class);
        assertEquals(Month.DECEMBER, value);
    }

    @Test
    public void testFormatAnnotation_zeroBased() throws Exception
    {
        Wrapper output = readerForZeroBased()
                .forType(Wrapper.class)
                .readValue("{\"value\":\"11\"}");
        assertEquals(new Wrapper(Month.DECEMBER).value, output.value);
    }

    @Test
    public void testFormatAnnotation_oneBased() throws Exception
    {
        Wrapper output = readerForOneBased()
                .forType(Wrapper.class)
                .readValue("{\"value\":\"11\"}");
        assertEquals(new Wrapper(Month.NOVEMBER).value, output.value);
    }

    /*
    /**********************************************************************
    /* Tests for empty string handling
    /**********************************************************************
     */

    @Test
    public void testDeserializeFromEmptyString() throws Exception
    {
        // Nulls are handled in general way, not by deserializer so they are ok
        Month m = MAPPER.readerFor(Month.class).readValue(" null ");
        assertNull(m);

        // Although coercion from empty String not enabled for Enums by default,
        // it IS for Scalars (when `MapperFeature.ALLOW_COERCION_OF_SCALARS` enabled
        // which it is by default). So need to disable it here:
        // (we no longer consider `Month` as LogicalType.Enum but LogicalType.DateTime)
        try {
            ObjectMapper strictMapper = mapperBuilder()
                    .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
                    .build();
            Month result = strictMapper.readerFor(Month.class).readValue("\"\"");
            fail("Should not pass, but got: " + result);
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot coerce empty String");
        }
        // But can allow coercion of empty String to, say, null
        ObjectMapper emptyStringMapper = mapperBuilder()
                .withCoercionConfig(Month.class,
                        h -> h.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull))
                .build();
        m = emptyStringMapper.readerFor(Month.class).readValue("\"\"");
        assertNull(m);
    }

    private ObjectReader readerForZeroBased() {
        return MAPPER
                .readerFor(Month.class)
                .without(DateTimeFeature.ONE_BASED_MONTHS);
    }

    private ObjectReader readerForOneBased() {
        return MAPPER
            .readerFor(Month.class)
            .with(DateTimeFeature.ONE_BASED_MONTHS);
    }
}