diff --git a/mapper/src/main/java/tw/com/softleader/data/jpa/spec/domain/SimpleSpecification.java b/mapper/src/main/java/tw/com/softleader/data/jpa/spec/domain/SimpleSpecification.java index 37c9aba..64777bf 100644 --- a/mapper/src/main/java/tw/com/softleader/data/jpa/spec/domain/SimpleSpecification.java +++ b/mapper/src/main/java/tw/com/softleader/data/jpa/spec/domain/SimpleSpecification.java @@ -26,6 +26,7 @@ import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Root; +import java.lang.reflect.InvocationTargetException; import java.util.Optional; import java.util.StringJoiner; import lombok.Builder; @@ -63,8 +64,22 @@ private static Specification newSpec( @NonNull Class domainClass, @NonNull String path, @NonNull Object value) { - return accessibleConstructor(domainClass, Context.class, String.class, Object.class) - .newInstance(context, path, value); + try { + return accessibleConstructor(domainClass, Context.class, String.class, Object.class) + .newInstance(context, path, value); + } catch (InvocationTargetException e) { + // Constructor.newInstance wraps any exception thrown inside the constructor in an + // InvocationTargetException; surface the original cause (e.g. TypeMismatchException, + // IllegalArgumentException) so it reaches SpecMapper.toSpec callers. + var cause = e.getCause(); + if (cause instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (cause instanceof Error error) { + throw error; + } + throw new IllegalStateException(cause); + } } @SuppressWarnings({"unchecked"}) diff --git a/mapper/src/test/java/tw/com/softleader/data/jpa/spec/domain/BetweenTest.java b/mapper/src/test/java/tw/com/softleader/data/jpa/spec/domain/BetweenTest.java index 15c89d6..60d6046 100644 --- a/mapper/src/test/java/tw/com/softleader/data/jpa/spec/domain/BetweenTest.java +++ b/mapper/src/test/java/tw/com/softleader/data/jpa/spec/domain/BetweenTest.java @@ -21,13 +21,19 @@ package tw.com.softleader.data.jpa.spec.domain; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static tw.com.softleader.data.jpa.spec.IntegrationTest.TestApplication.noopContext; import java.time.LocalDate; import java.util.Arrays; +import java.util.List; +import lombok.Builder; +import lombok.Data; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import tw.com.softleader.data.jpa.spec.IntegrationTest; +import tw.com.softleader.data.jpa.spec.SpecMapper; +import tw.com.softleader.data.jpa.spec.annotation.Spec; import tw.com.softleader.data.jpa.spec.usecase.Customer; import tw.com.softleader.data.jpa.spec.usecase.CustomerRepository; @@ -49,4 +55,22 @@ void test() { var actual = repository.findAll(spec); assertThat(actual).hasSize(1).contains(matt); } + + @Test + void oneElementThroughMapper() { + var mapper = SpecMapper.builder().build(); + var birthday = List.of(LocalDate.now()); + var criteria = BetweenCriteria.builder().birthday(birthday).build(); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> mapper.toSpec(criteria, Customer.class)) + .withMessage("@Between expected exact 2 elements, but was " + birthday); + } + + @Builder + @Data + static class BetweenCriteria { + + @Spec(value = Between.class) + List birthday; + } } diff --git a/mapper/src/test/java/tw/com/softleader/data/jpa/spec/domain/InTest.java b/mapper/src/test/java/tw/com/softleader/data/jpa/spec/domain/InTest.java index d01835e..b76149d 100644 --- a/mapper/src/test/java/tw/com/softleader/data/jpa/spec/domain/InTest.java +++ b/mapper/src/test/java/tw/com/softleader/data/jpa/spec/domain/InTest.java @@ -25,9 +25,13 @@ import static tw.com.softleader.data.jpa.spec.IntegrationTest.TestApplication.noopContext; import java.util.Arrays; +import lombok.Builder; +import lombok.Data; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import tw.com.softleader.data.jpa.spec.IntegrationTest; +import tw.com.softleader.data.jpa.spec.SpecMapper; +import tw.com.softleader.data.jpa.spec.annotation.Spec; import tw.com.softleader.data.jpa.spec.usecase.Customer; import tw.com.softleader.data.jpa.spec.usecase.CustomerRepository; @@ -56,4 +60,22 @@ void typeMismatch() { .withMessage( "Failed to convert value of type 'java.lang.Object' to required type 'java.lang.Iterable'"); } + + @Test + void typeMismatchThroughMapper() { + var mapper = SpecMapper.builder().build(); + var criteria = InCriteria.builder().name("matt").build(); + assertThatExceptionOfType(TypeMismatchException.class) + .isThrownBy(() -> mapper.toSpec(criteria, Customer.class)) + .withMessage( + "Failed to convert value of type 'java.lang.String' to required type 'java.lang.Iterable'"); + } + + @Builder + @Data + static class InCriteria { + + @Spec(path = "name", value = In.class) + String name; + } }