AdditionalOptionalNumbersTest.java

package tools.jackson.databind.ext.jdk8;

import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonInclude;

import tools.jackson.databind.*;
import tools.jackson.databind.exc.MismatchedInputException;
import tools.jackson.databind.testutil.DatabindTestUtil;

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

/**
 * Additional tests for OptionalInt, OptionalLong, and OptionalDouble
 * covering UNWRAP_SINGLE_VALUE_ARRAYS, boundary values, serialization
 * filters, and coercion edge cases.
 */
public class AdditionalOptionalNumbersTest
    extends DatabindTestUtil
{
    static class OptionalIntBean {
        public OptionalInt value;

        public OptionalIntBean() { value = OptionalInt.empty(); }
        OptionalIntBean(int v) { this(OptionalInt.of(v)); }
        OptionalIntBean(OptionalInt v) { value = v; }
    }

    static class OptionalLongBean {
        public OptionalLong value;

        public OptionalLongBean() { value = OptionalLong.empty(); }
        OptionalLongBean(long v) { this(OptionalLong.of(v)); }
        OptionalLongBean(OptionalLong v) { value = v; }
    }

    static class OptionalDoubleBean {
        public OptionalDouble value;

        public OptionalDoubleBean() { value = OptionalDouble.empty(); }
        OptionalDoubleBean(double v) { this(OptionalDouble.of(v)); }
        OptionalDoubleBean(OptionalDouble v) { value = v; }
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    /*
    /**********************************************************
    /* OptionalInt: boundary values
    /**********************************************************
     */

    @Test
    public void testOptionalIntBoundaryValues() throws Exception
    {
        OptionalIntBean maxBean = new OptionalIntBean(Integer.MAX_VALUE);
        String json = MAPPER.writeValueAsString(maxBean);
        OptionalIntBean result = MAPPER.readValue(json, OptionalIntBean.class);
        assertTrue(result.value.isPresent());
        assertEquals(Integer.MAX_VALUE, result.value.getAsInt());

        OptionalIntBean minBean = new OptionalIntBean(Integer.MIN_VALUE);
        json = MAPPER.writeValueAsString(minBean);
        result = MAPPER.readValue(json, OptionalIntBean.class);
        assertTrue(result.value.isPresent());
        assertEquals(Integer.MIN_VALUE, result.value.getAsInt());
    }

    @Test
    public void testOptionalIntZero() throws Exception
    {
        OptionalIntBean bean = new OptionalIntBean(0);
        String json = MAPPER.writeValueAsString(bean);
        OptionalIntBean result = MAPPER.readValue(json, OptionalIntBean.class);
        assertTrue(result.value.isPresent());
        assertEquals(0, result.value.getAsInt());
    }

    /*
    /**********************************************************
    /* OptionalInt: serialization filter
    /**********************************************************
     */

    @Test
    public void testOptionalIntSerializeFilter() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                .changeDefaultPropertyInclusion(
                        incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL))
                .build();
        assertEquals(a2q("{'value':123}"),
                mapper.writeValueAsString(new OptionalIntBean(123)));
        // absent is not strictly null so still serialized
        assertEquals(a2q("{'value':null}"),
                mapper.writeValueAsString(new OptionalIntBean()));

        mapper = jsonMapperBuilder()
                .changeDefaultPropertyInclusion(
                        incl -> incl.withValueInclusion(JsonInclude.Include.NON_ABSENT))
                .build();
        assertEquals(a2q("{'value':456}"),
                mapper.writeValueAsString(new OptionalIntBean(456)));
        assertEquals(a2q("{}"),
                mapper.writeValueAsString(new OptionalIntBean()));
    }

    /*
    /**********************************************************
    /* OptionalInt: UNWRAP_SINGLE_VALUE_ARRAYS
    /**********************************************************
     */

    @Test
    public void testOptionalIntUnwrapSingleValueArrays() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
                .build();
        OptionalInt result = mapper.readValue("[42]", OptionalInt.class);
        assertTrue(result.isPresent());
        assertEquals(42, result.getAsInt());
    }

    @Test
    public void testOptionalIntUnwrapSingleValueArraysDisabled() throws Exception
    {
        assertThrows(MismatchedInputException.class,
                () -> MAPPER.readValue("[42]", OptionalInt.class));
    }

    /*
    /**********************************************************
    /* OptionalInt: float-to-int coercion
    /**********************************************************
     */

    @Test
    public void testOptionalIntFromFloat() throws Exception
    {
        OptionalInt result = MAPPER.readValue("2.0", OptionalInt.class);
        assertTrue(result.isPresent());
        assertEquals(2, result.getAsInt());
    }

    /*
    /**********************************************************
    /* OptionalLong: UNWRAP_SINGLE_VALUE_ARRAYS
    /**********************************************************
     */

    @Test
    public void testOptionalLongUnwrapSingleValueArrays() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
                .build();
        OptionalLong result = mapper.readValue("[99]", OptionalLong.class);
        assertTrue(result.isPresent());
        assertEquals(99L, result.getAsLong());
    }

    @Test
    public void testOptionalLongUnwrapSingleValueArraysDisabled() throws Exception
    {
        assertThrows(MismatchedInputException.class,
                () -> MAPPER.readValue("[99]", OptionalLong.class));
    }

    /*
    /**********************************************************
    /* OptionalLong: float-to-long coercion
    /**********************************************************
     */

    @Test
    public void testOptionalLongFromFloat() throws Exception
    {
        OptionalLong result = MAPPER.readValue("3.0", OptionalLong.class);
        assertTrue(result.isPresent());
        assertEquals(3L, result.getAsLong());
    }

    /*
    /**********************************************************
    /* OptionalDouble: boundary values
    /**********************************************************
     */

    @Test
    public void testOptionalDoubleBoundaryValues() throws Exception
    {
        OptionalDoubleBean maxBean = new OptionalDoubleBean(Double.MAX_VALUE);
        String json = MAPPER.writeValueAsString(maxBean);
        OptionalDoubleBean result = MAPPER.readValue(json, OptionalDoubleBean.class);
        assertTrue(result.value.isPresent());
        assertEquals(Double.MAX_VALUE, result.value.getAsDouble());

        OptionalDoubleBean minBean = new OptionalDoubleBean(Double.MIN_VALUE);
        json = MAPPER.writeValueAsString(minBean);
        result = MAPPER.readValue(json, OptionalDoubleBean.class);
        assertTrue(result.value.isPresent());
        assertEquals(Double.MIN_VALUE, result.value.getAsDouble());
    }

    @Test
    public void testOptionalDoubleZero() throws Exception
    {
        OptionalDoubleBean bean = new OptionalDoubleBean(0.0);
        String json = MAPPER.writeValueAsString(bean);
        OptionalDoubleBean result = MAPPER.readValue(json, OptionalDoubleBean.class);
        assertTrue(result.value.isPresent());
        assertEquals(0.0, result.value.getAsDouble());
    }

    @Test
    public void testOptionalDoubleNegativeZero() throws Exception
    {
        OptionalDouble result = MAPPER.readValue("-0.0", OptionalDouble.class);
        assertTrue(result.isPresent());
        assertEquals(-0.0, result.getAsDouble());
    }

    /*
    /**********************************************************
    /* OptionalDouble: serialization filter
    /**********************************************************
     */

    @Test
    public void testOptionalDoubleSerializeFilter() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                .changeDefaultPropertyInclusion(
                        incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL))
                .build();
        assertEquals(a2q("{'value':1.5}"),
                mapper.writeValueAsString(new OptionalDoubleBean(1.5)));
        // absent is not strictly null so still serialized
        assertEquals(a2q("{'value':null}"),
                mapper.writeValueAsString(new OptionalDoubleBean()));

        mapper = jsonMapperBuilder()
                .changeDefaultPropertyInclusion(
                        incl -> incl.withValueInclusion(JsonInclude.Include.NON_ABSENT))
                .build();
        assertEquals(a2q("{'value':2.5}"),
                mapper.writeValueAsString(new OptionalDoubleBean(2.5)));
        assertEquals(a2q("{}"),
                mapper.writeValueAsString(new OptionalDoubleBean()));
    }

    /*
    /**********************************************************
    /* OptionalDouble: UNWRAP_SINGLE_VALUE_ARRAYS
    /**********************************************************
     */

    @Test
    public void testOptionalDoubleUnwrapSingleValueArrays() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
                .build();
        OptionalDouble result = mapper.readValue("[3.14]", OptionalDouble.class);
        assertTrue(result.isPresent());
        assertEquals(3.14, result.getAsDouble(), 0.001);
    }

    @Test
    public void testOptionalDoubleUnwrapSingleValueArraysDisabled() throws Exception
    {
        assertThrows(MismatchedInputException.class,
                () -> MAPPER.readValue("[3.14]", OptionalDouble.class));
    }

    /*
    /**********************************************************
    /* OptionalDouble: integer-to-double coercion
    /**********************************************************
     */

    @Test
    public void testOptionalDoubleFromInteger() throws Exception
    {
        OptionalDouble result = MAPPER.readValue("42", OptionalDouble.class);
        assertTrue(result.isPresent());
        assertEquals(42.0, result.getAsDouble());
    }

    /*
    /**********************************************************
    /* OptionalDouble: special values in bean context
    /**********************************************************
     */

    @Test
    public void testOptionalDoubleSpecialValuesRoundTrip() throws Exception
    {
        // NaN
        OptionalDouble nan = MAPPER.readValue(q("NaN"), OptionalDouble.class);
        assertTrue(nan.isPresent());
        assertTrue(Double.isNaN(nan.getAsDouble()));

        // Positive Infinity
        OptionalDouble posInf = MAPPER.readValue(q("Infinity"), OptionalDouble.class);
        assertTrue(posInf.isPresent());
        assertEquals(Double.POSITIVE_INFINITY, posInf.getAsDouble());

        // Negative Infinity
        OptionalDouble negInf = MAPPER.readValue(q("-Infinity"), OptionalDouble.class);
        assertTrue(negInf.isPresent());
        assertEquals(Double.NEGATIVE_INFINITY, negInf.getAsDouble());
    }

    /*
    /**********************************************************
    /* Cross-type: unexpected token handling
    /**********************************************************
     */

    @Test
    public void testOptionalIntFromBooleanFails() throws Exception
    {
        assertThrows(MismatchedInputException.class,
                () -> MAPPER.readValue("true", OptionalInt.class));
    }

    @Test
    public void testOptionalLongFromBooleanFails() throws Exception
    {
        assertThrows(MismatchedInputException.class,
                () -> MAPPER.readValue("true", OptionalLong.class));
    }

    @Test
    public void testOptionalDoubleFromBooleanFails() throws Exception
    {
        assertThrows(MismatchedInputException.class,
                () -> MAPPER.readValue("true", OptionalDouble.class));
    }

    @Test
    public void testOptionalIntFromObjectFails() throws Exception
    {
        assertThrows(MismatchedInputException.class,
                () -> MAPPER.readValue("{}", OptionalInt.class));
    }

    @Test
    public void testOptionalLongFromObjectFails() throws Exception
    {
        assertThrows(MismatchedInputException.class,
                () -> MAPPER.readValue("{}", OptionalLong.class));
    }

    @Test
    public void testOptionalDoubleFromObjectFails() throws Exception
    {
        assertThrows(MismatchedInputException.class,
                () -> MAPPER.readValue("{}", OptionalDouble.class));
    }
}