Skip to content

Commit

Permalink
Refactor: AutoConverter & tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
driessamyn committed Jan 29, 2025
1 parent 9111fe4 commit 93b692f
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 177 deletions.
91 changes: 17 additions & 74 deletions core/src/main/kotlin/net/samyn/kapper/internal/AutoConverter.kt
Original file line number Diff line number Diff line change
@@ -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<KClass<*>, (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}",
)
}
87 changes: 87 additions & 0 deletions core/src/main/kotlin/net/samyn/kapper/internal/Converters.kt
Original file line number Diff line number Diff line change
@@ -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())
}
2 changes: 1 addition & 1 deletion core/src/main/kotlin/net/samyn/kapper/internal/Mapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlin.reflect.full.primaryConstructor

internal class Mapper<T : Any>(
private val clazz: Class<T>,
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<T> =
Expand Down
159 changes: 57 additions & 102 deletions core/src/test/kotlin/net/samyn/kapper/AutoConverterTest.kt
Original file line number Diff line number Diff line change
@@ -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<KClass<*>, (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<KapperUnsupportedOperationException> {
AutoConverter.convert(123, UUID::class)
}
}

@ParameterizedTest
@ValueSource(strings = ["", "123e4567-e89b-12d3-a456-426614ZZZZZZ"])
fun `when invalid string cannot convert to UUID`(input: String) {
shouldThrow<KapperParseException> {
AutoConverter.convert(input, UUID::class)
autoConverter.convert("123", Int::class)
}
}

@Test
fun `when invalid target type throw`() {
shouldThrow<KapperUnsupportedOperationException> {
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<KapperUnsupportedOperationException> {
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<KapperUnsupportedOperationException> {
AutoConverter.convert("2023-01-01", LocalDate::class) // Invalid input type
@Test
fun `supports UUID`() {
shouldNotThrow<KapperUnsupportedOperationException> {
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<KapperUnsupportedOperationException> {
AutoConverter.convert(Date(), Instant::class) // Invalid input type
@Test
fun `supports LocalDate`() {
shouldNotThrow<KapperUnsupportedOperationException> {
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<KapperUnsupportedOperationException> {
AutoConverter.convert(Date(), LocalDateTime::class) // Invalid input type
@Test
fun `supports LocalDateTime`() {
shouldNotThrow<KapperUnsupportedOperationException> {
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<KapperUnsupportedOperationException> {
AutoConverter.convert(Date(), LocalTime::class) // Invalid input type
@Test
fun `supports LocalTime`() {
shouldNotThrow<KapperUnsupportedOperationException> {
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<KapperUnsupportedOperationException> {
autoConverter.convert(LocalTime.of(12, 30), Instant::class)
}
}
}
}
Loading

0 comments on commit 93b692f

Please sign in to comment.