FunctionalScalarDeserializer4004Test.java

package tools.jackson.databind.deser;

import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.Test;

import tools.jackson.core.JsonParser;
import tools.jackson.core.type.TypeReference;

import tools.jackson.databind.DatabindException;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.cfg.CoercionAction;
import tools.jackson.databind.cfg.CoercionInputShape;
import tools.jackson.databind.deser.std.FunctionalScalarDeserializer;
import tools.jackson.databind.exc.MismatchedInputException;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.type.LogicalType;

import static org.junit.jupiter.api.Assertions.*;
import static tools.jackson.databind.testutil.DatabindTestUtil.*;

// [databind#4004]: Add FunctionalScalarDeserializer for functional-style deserialization
public class FunctionalScalarDeserializer4004Test
{
    // Simple value type for testing
    static class Bar {
        private final String value;

        private Bar(String value) {
            this.value = value;
        }

        public static Bar of(String value) {
            return new Bar(value);
        }

        public String getValue() {
            return value;
        }
    }

    // Wrapper POJO for testing deserialization as a field
    static class BarWrapper {
        public Bar bar;
    }

    @Test
    public void testClassWithFunction() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("\"hello\"", Bar.class);
        assertEquals("hello", result.getValue());
    }

    @Test
    public void testJavaTypeWithFunction() throws Exception
    {
        ObjectMapper baseMapper = jsonMapperBuilder().build();
        JavaType barType = baseMapper.constructType(Bar.class);

        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(barType, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("\"javatype\"", Bar.class);
        assertEquals("javatype", result.getValue());
    }

    @Test
    public void testClassWithBiFunction() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class,
                        (p, ctx) -> Bar.of("prefix:" + p.getValueAsString())));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("\"test\"", Bar.class);
        assertEquals("prefix:test", result.getValue());
    }

    @Test
    public void testJavaTypeWithBiFunction() throws Exception
    {
        ObjectMapper baseMapper = jsonMapperBuilder().build();
        JavaType barType = baseMapper.constructType(Bar.class);

        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(barType,
                        (p, ctx) -> Bar.of("bi:" + p.getValueAsString())));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("\"test\"", Bar.class);
        assertEquals("bi:test", result.getValue());
    }

    @Test
    public void testFromIntegerNumber() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("123", Bar.class);
        assertEquals("123", result.getValue());
    }

    @Test
    public void testFromDecimalNumber() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("3.14159", Bar.class);
        assertEquals("3.14159", result.getValue());
    }

    @Test
    public void testFromNegativeNumber() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("-42", Bar.class);
        assertEquals("-42", result.getValue());
    }

    @Test
    public void testFromBooleanTrue() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("true", Bar.class);
        assertEquals("true", result.getValue());
    }

    @Test
    public void testFromBooleanFalse() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("false", Bar.class);
        assertEquals("false", result.getValue());
    }

    @Test
    public void testNullValue() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("null", Bar.class);
        assertNull(result);
    }

    @Test
    public void testEmptyStringDefault() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        // By default, empty string returns null for OtherScalar type
        Bar result = mapper.readValue("\"\"", Bar.class);
        assertNull(result);
    }

    @Test
    public void testEmptyStringCoercionFail() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .withCoercionConfigDefaults(cfg -> cfg.setCoercion(
                        CoercionInputShape.EmptyString, CoercionAction.Fail))
                .build();

        try {
            mapper.readValue("\"\"", Bar.class);
            fail("Should throw exception for empty string with Fail action");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot coerce empty String");
        }
    }

    @Test
    public void testEmptyStringCoercionAsNull() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .withCoercionConfig(LogicalType.OtherScalar, cfg -> cfg.setCoercion(
                        CoercionInputShape.EmptyString, CoercionAction.AsNull))
                .build();

        Bar result = mapper.readValue("\"\"", Bar.class);
        assertNull(result);
    }

    @Test
    public void testEmptyStringCoercionAsEmpty() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .withCoercionConfig(LogicalType.OtherScalar, cfg -> cfg.setCoercion(
                        CoercionInputShape.EmptyString, CoercionAction.AsEmpty))
                .build();

        // AsEmpty typically returns null for types without defined empty value
        Bar result = mapper.readValue("\"\"", Bar.class);
        assertNull(result);
    }

    @Test
    public void testAsPojoField() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        BarWrapper result = mapper.readValue("{\"bar\":\"fieldValue\"}", BarWrapper.class);
        assertNotNull(result.bar);
        assertEquals("fieldValue", result.bar.getValue());
    }

    @Test
    public void testInList() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        List<Bar> result = mapper.readValue("[\"a\", \"b\", \"c\"]",
                new TypeReference<List<Bar>>() {});
        assertEquals(3, result.size());
        assertEquals("a", result.get(0).getValue());
        assertEquals("b", result.get(1).getValue());
        assertEquals("c", result.get(2).getValue());
    }

    @Test
    public void testNullFieldInPojo() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        BarWrapper result = mapper.readValue("{\"bar\":null}", BarWrapper.class);
        assertNull(result.bar);
    }

    @Test
    public void testRejectsJsonArray() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        try {
            mapper.readValue("[\"hello\"]", Bar.class);
            fail("Should not accept JSON array");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot deserialize");
        }
    }

    @Test
    public void testRejectsJsonObject() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, Bar::of));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        try {
            mapper.readValue("{\"value\":\"hello\"}", Bar.class);
            fail("Should not accept JSON object");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot deserialize");
        }
    }

    @Test
    public void testFunctionThrowsIllegalArgumentException() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, s -> {
                    throw new IllegalArgumentException("Invalid format: " + s);
                }));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        try {
            mapper.readValue("\"bad\"", Bar.class);
            fail("Should throw exception");
        } catch (MismatchedInputException e) {
            verifyException(e, "not a valid textual representation");
            verifyException(e, "Invalid format");
        }
    }

    @Test
    public void testBiFunctionThrowsIllegalArgumentException() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, (p, ctx) -> {
                    throw new IllegalArgumentException("BiFunction error: " + p.getValueAsString());
                }));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        try {
            mapper.readValue("\"invalid\"", Bar.class);
            fail("Should throw exception");
        } catch (MismatchedInputException e) {
            verifyException(e, "not a valid textual representation");
            verifyException(e, "BiFunction error");
        }
    }

    @Test
    public void testStringFunctionReceivesExtractedText() throws Exception
    {
        final AtomicReference<String> receivedValue = new AtomicReference<>();

        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, text -> {
                    receivedValue.set(text);
                    return Bar.of(text);
                }));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("\"expected-value\"", Bar.class);

        assertEquals("expected-value", receivedValue.get());
        assertEquals("expected-value", result.getValue());
    }

    @Test
    public void testBiFunctionReceivesParserDirectly() throws Exception
    {
        final AtomicReference<String> parserState = new AtomicReference<>();

        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, (p, ctx) -> {
                    parserState.set(p.currentToken().toString());
                    return Bar.of(p.getValueAsString());
                }));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("\"test-value\"", Bar.class);

        assertEquals("VALUE_STRING", parserState.get());
        assertEquals("test-value", result.getValue());
    }

    @Test
    public void testStringFunctionReceivesCoercedNumericText() throws Exception
    {
        final AtomicReference<String> receivedValue = new AtomicReference<>();

        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, text -> {
                    receivedValue.set(text);
                    return Bar.of(text);
                }));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        Bar result = mapper.readValue("12345", Bar.class);

        assertEquals("12345", receivedValue.get());
        assertEquals("12345", result.getValue());
    }

    @Test
    public void testFunctionThrowsCustomException() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, s -> {
                    throw new RuntimeException("Custom error: " + s);
                }));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        try {
            mapper.readValue("\"bad\"", Bar.class);
            fail("Should throw exception");
        } catch (MismatchedInputException e) {
            verifyException(e, "not a valid textual representation");
            verifyException(e, "Custom error");
        }
    }

    @Test
    public void testBiFunctionThrowsCustomException() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, (p, ctx) -> {
                    throw new RuntimeException("BiFunction custom error: " + p.getValueAsString());
                }));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        try {
            mapper.readValue("\"invalid\"", Bar.class);
            fail("Should throw exception");
        } catch (MismatchedInputException e) {
            verifyException(e, "not a valid textual representation");
            verifyException(e, "BiFunction custom error");
        }
    }

    @Test
    public void testFunctionThrowsJacksonExceptionPropagatedAsIs() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, s -> {
                    throw DatabindException.from((JsonParser) null, "User JacksonException");
                }));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        try {
            mapper.readValue("\"test\"", Bar.class);
            fail("Should throw exception");
        } catch (DatabindException e) {
            assertEquals("User JacksonException", e.getOriginalMessage());
        }
    }

    @Test
    public void testBiFunctionThrowsJacksonExceptionPropagatedAsIs() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, (p, ctx) -> {
                    throw DatabindException.from(p, "User BiFunction JacksonException");
                }));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();

        try {
            mapper.readValue("\"test\"", Bar.class);
            fail("Should throw exception");
        } catch (DatabindException e) {
            assertEquals("User BiFunction JacksonException", e.getOriginalMessage());
        }
    }

    @Test
    public void testWrapExceptionsEnabled() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, s -> {
                    throw new RuntimeException("User error");
                }));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .enable(DeserializationFeature.WRAP_EXCEPTIONS)
                .build();

        try {
            mapper.readValue("\"test\"", Bar.class);
            fail("Should throw exception");
        } catch (MismatchedInputException e) {
            verifyException(e, "not a valid textual representation");
            verifyException(e, "User error");
        } catch (Exception e) {
            fail("Should wrap as MismatchedInputException, got: " + e.getClass().getName());
        }
    }

    @Test
    public void testWrapExceptionsDisabled() throws Exception
    {
        SimpleModule module = new SimpleModule("test");
        module.addDeserializer(Bar.class,
                new FunctionalScalarDeserializer<>(Bar.class, s -> {
                    throw new RuntimeException("User error");
                }));

        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .disable(DeserializationFeature.WRAP_EXCEPTIONS)
                .build();

        try {
            mapper.readValue("\"test\"", Bar.class);
            fail("Should throw exception");
        } catch (MismatchedInputException e) {
            fail("Should not wrap exception when WRAP_EXCEPTIONS is disabled");
        } catch (RuntimeException e) {
            verifyException(e, "User error");
        }
    }
}