From 93b692fb52535ee43e3c8985c139072712d174ea Mon Sep 17 00:00:00 2001 From: Dries Samyn <5557551+driessamyn@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:38:57 +0000 Subject: [PATCH] Refactor: AutoConverter & tests. --- .../samyn/kapper/internal/AutoConverter.kt | 91 ++-------- .../net/samyn/kapper/internal/Converters.kt | 87 ++++++++++ .../net/samyn/kapper/internal/Mapper.kt | 2 +- .../net/samyn/kapper/AutoConverterTest.kt | 159 +++++++----------- .../kotlin/net/samyn/kapper/ConverterTest.kt | 148 ++++++++++++++++ 5 files changed, 310 insertions(+), 177 deletions(-) create mode 100644 core/src/main/kotlin/net/samyn/kapper/internal/Converters.kt create mode 100644 core/src/test/kotlin/net/samyn/kapper/ConverterTest.kt diff --git a/core/src/main/kotlin/net/samyn/kapper/internal/AutoConverter.kt b/core/src/main/kotlin/net/samyn/kapper/internal/AutoConverter.kt index e16b6a7..020862c 100644 --- a/core/src/main/kotlin/net/samyn/kapper/internal/AutoConverter.kt +++ b/core/src/main/kotlin/net/samyn/kapper/internal/AutoConverter.kt @@ -1,88 +1,31 @@ package net.samyn.kapper.internal -import net.samyn.kapper.KapperParseException import net.samyn.kapper.KapperUnsupportedOperationException -import java.nio.ByteBuffer import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime -import java.util.Calendar -import java.util.Date import java.util.UUID import kotlin.reflect.KClass -internal object AutoConverter { +internal class AutoConverter( + // register converters + // we can/should extend this to allow users to register custom converters. + private val converters: Map, (Any) -> Any> = + mapOf( + UUID::class to ::convertUUID, + LocalDate::class to ::convertLocalDate, + LocalDateTime::class to ::convertLocalDateTime, + LocalTime::class to ::convertLocalTime, + Instant::class to ::convertInstant, + ), +) { fun convert( value: Any, target: KClass<*>, - ): Any { - val converted = - when (target) { - UUID::class -> { - if (value is String) { - try { - UUID.fromString(value) - } catch (e: Exception) { - throw KapperParseException( - "Cannot parse $value to UUID", - e, - ) - } - } else if (value is ByteArray) { - value.asUUID() - } else { - throw KapperUnsupportedOperationException( - "Cannot auto-convert from ${value.javaClass} to ${target.qualifiedName}", - ) - } - } - LocalDate::class -> - if (value is Date) { - val cal = Calendar.getInstance() - cal.time = value - LocalDate.of(cal[Calendar.YEAR], cal[Calendar.MONTH] + 1, cal[Calendar.DAY_OF_MONTH]) - } else { - throw KapperUnsupportedOperationException( - "Cannot auto-convert from ${value.javaClass} to ${target.qualifiedName}", - ) - } - LocalDateTime::class -> - if (value is Instant) { - LocalDateTime.ofInstant(value, java.time.ZoneOffset.UTC) - } else { - throw KapperUnsupportedOperationException( - "Cannot auto-convert from ${value.javaClass} to ${target.qualifiedName}", - ) - } - LocalTime::class -> - if (value is Instant) { - LocalTime.ofInstant(value, java.time.ZoneOffset.UTC) - } else { - throw KapperUnsupportedOperationException( - "Cannot auto-convert from ${value.javaClass} to ${target.qualifiedName}", - ) - } - Instant::class -> - if (value is LocalTime) { - value.atDate(LocalDate.now()).toInstant(java.time.ZoneOffset.UTC) - } else if (value is LocalDateTime) { - value.atZone(java.time.ZoneOffset.UTC).toInstant() - } else { - throw KapperUnsupportedOperationException( - "Cannot auto-convert from ${value.javaClass} to ${target.qualifiedName}", - ) - } - else -> - throw KapperUnsupportedOperationException( - "Cannot auto-convert from ${value.javaClass} to ${target.qualifiedName}", - ) - } - return converted - } - - private fun ByteArray.asUUID(): UUID { - val b = ByteBuffer.wrap(this) - return UUID(b.getLong(), b.getLong()) - } + ): Any = + // Kover considers this not covered, which I think is a bug. Will reproduce in separate project and raise issue. + converters[target]?.invoke(value) ?: throw KapperUnsupportedOperationException( + "Cannot auto-convert from ${value.javaClass} to ${target.qualifiedName}", + ) } diff --git a/core/src/main/kotlin/net/samyn/kapper/internal/Converters.kt b/core/src/main/kotlin/net/samyn/kapper/internal/Converters.kt new file mode 100644 index 0000000..f4f35c4 --- /dev/null +++ b/core/src/main/kotlin/net/samyn/kapper/internal/Converters.kt @@ -0,0 +1,87 @@ +package net.samyn.kapper.internal + +import net.samyn.kapper.KapperParseException +import net.samyn.kapper.KapperUnsupportedOperationException +import java.nio.ByteBuffer +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.util.Calendar +import java.util.Date +import java.util.UUID + +internal fun convertInstant(value: Any): Instant = + when (value) { + is LocalTime -> { + value.atDate(LocalDate.now()).toInstant(java.time.ZoneOffset.UTC) + } + + is LocalDateTime -> { + value.atZone(java.time.ZoneOffset.UTC).toInstant() + } + + else -> { + throw KapperUnsupportedOperationException( + "Cannot auto-convert from ${value.javaClass} to Instant", + ) + } + } + +internal fun convertLocalTime(value: Any): LocalTime = + if (value is Instant) { + LocalTime.ofInstant(value, java.time.ZoneOffset.UTC) + } else { + throw KapperUnsupportedOperationException( + "Cannot auto-convert from ${value.javaClass} to LocalTime", + ) + } + +internal fun convertLocalDateTime(value: Any): LocalDateTime = + if (value is Instant) { + LocalDateTime.ofInstant(value, java.time.ZoneOffset.UTC) + } else { + throw KapperUnsupportedOperationException( + "Cannot auto-convert from ${value.javaClass} to LocalDateTime", + ) + } + +internal fun convertLocalDate(value: Any): LocalDate = + if (value is Date) { + val cal = Calendar.getInstance() + cal.time = value + LocalDate.of(cal[Calendar.YEAR], cal[Calendar.MONTH] + 1, cal[Calendar.DAY_OF_MONTH]) + } else { + throw KapperUnsupportedOperationException( + "Cannot auto-convert from ${value.javaClass} to LocalDate", + ) + } + +internal fun convertUUID(value: Any): UUID = + when (value) { + is String -> { + try { + UUID.fromString(value) + } catch (e: Exception) { + throw KapperParseException( + "Cannot parse $value to UUID", + e, + ) + } + } + + is ByteArray -> { + value.asUUID() + } + + else -> { + throw KapperUnsupportedOperationException( + "Cannot auto-convert from ${value.javaClass} to UUID", + ) + } + } + +fun ByteArray.asUUID(): UUID { + val b = ByteBuffer.wrap(this) + return UUID(b.getLong(), b.getLong()) +} diff --git a/core/src/main/kotlin/net/samyn/kapper/internal/Mapper.kt b/core/src/main/kotlin/net/samyn/kapper/internal/Mapper.kt index 93b0e82..c957e67 100644 --- a/core/src/main/kotlin/net/samyn/kapper/internal/Mapper.kt +++ b/core/src/main/kotlin/net/samyn/kapper/internal/Mapper.kt @@ -10,7 +10,7 @@ import kotlin.reflect.full.primaryConstructor internal class Mapper( private val clazz: Class, - val autoConverter: (Any, KClass<*>) -> Any = AutoConverter::convert, + val autoConverter: (Any, KClass<*>) -> Any = AutoConverter()::convert, val sqlTypesConverter: (JDBCType, String, ResultSet, Int) -> Any? = SQLTypesConverter::convertSQLType, ) { private val constructor: KFunction = diff --git a/core/src/test/kotlin/net/samyn/kapper/AutoConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/AutoConverterTest.kt index ec41ba7..b15c817 100644 --- a/core/src/test/kotlin/net/samyn/kapper/AutoConverterTest.kt +++ b/core/src/test/kotlin/net/samyn/kapper/AutoConverterTest.kt @@ -1,135 +1,90 @@ package net.samyn.kapper +import io.kotest.assertions.throwables.shouldNotThrow import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import net.samyn.kapper.internal.AutoConverter +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import java.nio.ByteBuffer import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.util.Date import java.util.UUID +import kotlin.reflect.KClass class AutoConverterTest { - @Test - fun `when string UUID convert`() { - val uuid = "123e4567-e89b-12d3-a456-426614174000" - val autoConvertedUuid = AutoConverter.convert(uuid, UUID::class) - autoConvertedUuid.shouldBe(UUID.fromString(uuid)) - } - - @Test - fun `when binary UUID convert`() { - val uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000") - val autoConvertedUuid = AutoConverter.convert(uuid.asBytes(), UUID::class) - autoConvertedUuid.shouldBe(uuid) - } + private val mockConverter = mockk<(Any) -> Any>() + private val mockConverters = + mapOf, (Any) -> Any>( + String::class to mockConverter, + ) @Test - fun `when int cannot convert to UUID`() { + fun `when converters not available throw`() { + val autoConverter = AutoConverter(mockConverters) shouldThrow { - AutoConverter.convert(123, UUID::class) - } - } - - @ParameterizedTest - @ValueSource(strings = ["", "123e4567-e89b-12d3-a456-426614ZZZZZZ"]) - fun `when invalid string cannot convert to UUID`(input: String) { - shouldThrow { - AutoConverter.convert(input, UUID::class) + autoConverter.convert("123", Int::class) } } @Test - fun `when invalid target type throw`() { - shouldThrow { - AutoConverter.convert("123", String::class) + fun `when converters available call`() { + every { mockConverter.invoke("123") } returns "123" + val autoConverter = AutoConverter(mockConverters) + val converted = autoConverter.convert("123", String::class) + verify { mockConverter.invoke("123") } + converted shouldBe "123" + } + + @Nested + inner class DefaultConverters { + private val autoConverter = AutoConverter() + + @Test + fun `when invalid target type throw`() { + shouldThrow { + autoConverter.convert("123", Int::class) + } } - } - @Test - fun `convert valid Date to LocalDate`() { - val date = Date.from(Instant.parse("2023-10-01T00:00:00Z")) - val localDate = AutoConverter.convert(date, LocalDate::class) as LocalDate - localDate.shouldBe(LocalDate.of(2023, 10, 1)) - } - - @Test - fun `throw exception when invalid type is converted to LocalDate`() { - shouldThrow { - AutoConverter.convert("2023-01-01", LocalDate::class) // Invalid input type + @Test + fun `supports UUID`() { + shouldNotThrow { + autoConverter.convert("123e4567-e89b-12d3-a456-426614174000", UUID::class) + } } - } - @Test - fun `convert valid LocalTime to Instant`() { - val time = LocalTime.of(12, 30) // 12:30 PM - val instant = AutoConverter.convert(time, Instant::class) as Instant - val expectedInstant = LocalDate.now().atTime(12, 30).toInstant(java.time.ZoneOffset.UTC) - instant.shouldBe(expectedInstant) - } - - @Test - fun `convert LocalTime at midnight to Instant`() { - val midnight = LocalTime.MIDNIGHT - val instant = AutoConverter.convert(midnight, Instant::class) as Instant - val expectedInstant = LocalDate.now().atTime(0, 0).toInstant(java.time.ZoneOffset.UTC) - instant.shouldBe(expectedInstant) - } - - @Test - fun `convert valid LocalDateTime to Instant`() { - val now = LocalDateTime.now() - val instant = AutoConverter.convert(now, Instant::class) as Instant - val expectedInstant = now.toInstant(java.time.ZoneOffset.UTC) - instant.shouldBe(expectedInstant) - } - - @Test - fun `throw exception when invalid type is converted to Instant`() { - shouldThrow { - AutoConverter.convert(Date(), Instant::class) // Invalid input type + @Test + fun `supports LocalDate`() { + shouldNotThrow { + autoConverter.convert(Date.from(Instant.parse("2023-10-01T00:00:00Z")), LocalDate::class) + } } - } - - @Test - fun `convert valid Instant to LocalDateTime`() { - val now = Instant.now() - val localDateTime = AutoConverter.convert(now, LocalDateTime::class) as LocalDateTime - val expectedDt = LocalDateTime.ofInstant(now, java.time.ZoneOffset.UTC) - localDateTime.shouldBe(expectedDt) - } - @Test - fun `throw exception when invalid type is converted to LocalDateTime`() { - shouldThrow { - AutoConverter.convert(Date(), LocalDateTime::class) // Invalid input type + @Test + fun `supports LocalDateTime`() { + shouldNotThrow { + autoConverter.convert(Instant.now(), LocalDateTime::class) + } } - } - - @Test - fun `convert valid Instant to LocalTime`() { - val now = Instant.now() - val locaTime = AutoConverter.convert(now, LocalTime::class) as LocalTime - val expectedTime = LocalTime.ofInstant(now, java.time.ZoneOffset.UTC) - locaTime.shouldBe(expectedTime) - } - @Test - fun `throw exception when invalid type is converted to LocalTime`() { - shouldThrow { - AutoConverter.convert(Date(), LocalTime::class) // Invalid input type + @Test + fun `supports LocalTime`() { + shouldNotThrow { + autoConverter.convert(Instant.now(), LocalTime::class) + } } - } - private fun UUID.asBytes(): ByteArray { - val b = ByteBuffer.wrap(ByteArray(16)) - b.putLong(mostSignificantBits) - b.putLong(leastSignificantBits) - return b.array() + @Test + fun `supports Instant`() { + shouldNotThrow { + autoConverter.convert(LocalTime.of(12, 30), Instant::class) + } + } } } diff --git a/core/src/test/kotlin/net/samyn/kapper/ConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/ConverterTest.kt new file mode 100644 index 0000000..43713ff --- /dev/null +++ b/core/src/test/kotlin/net/samyn/kapper/ConverterTest.kt @@ -0,0 +1,148 @@ +package net.samyn.kapper + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import net.samyn.kapper.internal.convertInstant +import net.samyn.kapper.internal.convertLocalDate +import net.samyn.kapper.internal.convertLocalDateTime +import net.samyn.kapper.internal.convertLocalTime +import net.samyn.kapper.internal.convertUUID +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import java.nio.ByteBuffer +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.util.Date +import java.util.UUID + +class ConverterTest { + @Nested + inner class UUIDConverter { + @Test + fun `when string UUID convert`() { + val uuid = "123e4567-e89b-12d3-a456-426614174000" + val autoConvertedUuid = convertUUID(uuid) + autoConvertedUuid.shouldBe(UUID.fromString(uuid)) + } + + @Test + fun `when binary UUID convert`() { + val uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000") + val autoConvertedUuid = convertUUID(uuid.asBytes()) + autoConvertedUuid.shouldBe(uuid) + } + + @Test + fun `when int cannot convert to UUID`() { + shouldThrow { + convertUUID(123) + } + } + + @ParameterizedTest + @ValueSource(strings = ["", "123e4567-e89b-12d3-a456-426614ZZZZZZ"]) + fun `when invalid string cannot convert to UUID`(input: String) { + shouldThrow { + convertUUID(input) + } + } + } + + @Nested + inner class LocalDateConverter { + @Test + fun `convert valid Date to LocalDate`() { + val date = Date.from(Instant.parse("2023-10-01T00:00:00Z")) + val localDate = convertLocalDate(date) + localDate.shouldBe(LocalDate.of(2023, 10, 1)) + } + + @Test + fun `throw exception when invalid type is converted to LocalDate`() { + shouldThrow { + convertLocalDate("2023-01-01") // Invalid input type + } + } + } + + @Nested + inner class InstantConverter { + @Test + fun `convert valid LocalTime to Instant`() { + val time = LocalTime.of(12, 30) // 12:30 PM + val instant = convertInstant(time) as Instant + val expectedInstant = LocalDate.now().atTime(12, 30).toInstant(java.time.ZoneOffset.UTC) + instant.shouldBe(expectedInstant) + } + + @Test + fun `convert LocalTime at midnight to Instant`() { + val midnight = LocalTime.MIDNIGHT + val instant = convertInstant(midnight) as Instant + val expectedInstant = LocalDate.now().atTime(0, 0).toInstant(java.time.ZoneOffset.UTC) + instant.shouldBe(expectedInstant) + } + + @Test + fun `convert valid LocalDateTime to Instant`() { + val now = LocalDateTime.now() + val instant = convertInstant(now) as Instant + val expectedInstant = now.toInstant(java.time.ZoneOffset.UTC) + instant.shouldBe(expectedInstant) + } + + @Test + fun `throw exception when invalid type is converted to Instant`() { + shouldThrow { + convertInstant(Date()) // Invalid input type + } + } + } + + @Nested + inner class LocalDateTimeConverter { + @Test + fun `convert valid Instant to LocalDateTime`() { + val now = Instant.now() + val localDateTime = convertLocalDateTime(now) as LocalDateTime + val expectedDt = LocalDateTime.ofInstant(now, java.time.ZoneOffset.UTC) + localDateTime.shouldBe(expectedDt) + } + + @Test + fun `throw exception when invalid type is converted to LocalDateTime`() { + shouldThrow { + convertLocalDateTime(Date()) // Invalid input type + } + } + } + + @Nested + inner class LocalTimeConverter { + @Test + fun `convert valid Instant to LocalTime`() { + val now = Instant.now() + val locaTime = convertLocalTime(now) as LocalTime + val expectedTime = LocalTime.ofInstant(now, java.time.ZoneOffset.UTC) + locaTime.shouldBe(expectedTime) + } + + @Test + fun `throw exception when invalid type is converted to LocalTime`() { + shouldThrow { + convertLocalTime(Date()) // Invalid input type + } + } + } + + private fun UUID.asBytes(): ByteArray { + val b = ByteBuffer.wrap(ByteArray(16)) + b.putLong(mostSignificantBits) + b.putLong(leastSignificantBits) + return b.array() + } +}