diff --git a/build.gradle.kts b/build.gradle.kts index cec3497..2ed2a7c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.mapk" -version = "0.24" +version = "0.25" java { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/src/main/kotlin/com/mapk/conversion/KConvert.kt b/src/main/kotlin/com/mapk/conversion/KConvert.kt new file mode 100644 index 0000000..8b6f226 --- /dev/null +++ b/src/main/kotlin/com/mapk/conversion/KConvert.kt @@ -0,0 +1,13 @@ +package com.mapk.conversion + +import kotlin.reflect.KClass + +@Target(AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class KConvertBy(val converters: Array>>) + +abstract class AbstractKConverter(protected val annotation: A) { + abstract val srcClass: KClass + abstract fun convert(source: S?): D? +} diff --git a/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt b/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt index e3a3701..adac56a 100644 --- a/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt +++ b/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt @@ -79,7 +79,7 @@ internal sealed class BoundParameterForMap { val propertyClazz = property.returnType.classifier as KClass<*> // コンバータが取れた場合 - paramClazz.getConverters() + (param.getConverters() + paramClazz.getConverters()) .filter { (key, _) -> propertyClazz.isSubclassOf(key) } .let { if (1 < it.size) throw IllegalArgumentException("${param.name} has multiple converter. $it") diff --git a/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt b/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt index 92dc333..92c5a27 100644 --- a/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt +++ b/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt @@ -17,7 +17,9 @@ internal class ParameterForMap private constructor( clazz.java } // リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい - private val converters: Set, KFunction>> = clazz.getConverters() + @Suppress("UNCHECKED_CAST") + private val converters: Set, KFunction>> = + (param.getConverters() as Set, KFunction>>) + clazz.getConverters() private val convertCache: ConcurrentMap, ParameterProcessor> = ConcurrentHashMap() diff --git a/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt b/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt index 4a109d4..25c395d 100644 --- a/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt +++ b/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt @@ -1,19 +1,23 @@ package com.mapk.kmapper import com.mapk.annotations.KConverter +import com.mapk.conversion.KConvertBy import com.mapk.core.KFunctionWithInstance import kotlin.reflect.KClass import kotlin.reflect.KFunction +import kotlin.reflect.KParameter import kotlin.reflect.full.companionObjectInstance +import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.functions import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.full.primaryConstructor import kotlin.reflect.full.staticFunctions import kotlin.reflect.jvm.isAccessible internal fun KClass.getConverters(): Set, KFunction>> = convertersFromConstructors(this) + convertersFromStaticMethods(this) + convertersFromCompanionObject(this) -private fun Collection>.getConverterMapFromFunctions(): Set, KFunction>> { +private fun Collection>.getConvertersFromFunctions(): Set, KFunction>> { return filter { it.annotations.any { annotation -> annotation is KConverter } } .map { func -> func.isAccessible = true @@ -23,14 +27,14 @@ private fun Collection>.getConverterMapFromFunctions(): Set convertersFromConstructors(clazz: KClass): Set, KFunction>> { - return clazz.constructors.getConverterMapFromFunctions() + return clazz.constructors.getConvertersFromFunctions() } @Suppress("UNCHECKED_CAST") private fun convertersFromStaticMethods(clazz: KClass): Set, KFunction>> { val staticFunctions: Collection> = clazz.staticFunctions as Collection> - return staticFunctions.getConverterMapFromFunctions() + return staticFunctions.getConvertersFromFunctions() } @Suppress("UNCHECKED_CAST") @@ -49,6 +53,16 @@ private fun convertersFromCompanionObject(clazz: KClass): Set, KFunction<*>>> { + return annotations.mapNotNull { paramAnnotation -> + paramAnnotation.annotationClass + .findAnnotation() + ?.converters + ?.map { it.primaryConstructor!!.call(paramAnnotation) } + }.flatten().map { (it.srcClass) to it::convert as KFunction<*> }.toSet() +} + // 引数の型がconverterに対して入力可能ならconverterを返す internal fun Set, KFunction>>.getConverter(input: KClass): KFunction? = this.find { (key, _) -> input.isSubclassOf(key) }?.second diff --git a/src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt b/src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt index 6273870..cc934e0 100644 --- a/src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt +++ b/src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt @@ -15,7 +15,9 @@ internal class PlainParameterForMap private constructor( clazz.java } // リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい - private val converters: Set, KFunction>> = clazz.getConverters() + @Suppress("UNCHECKED_CAST") + private val converters: Set, KFunction>> = + (param.getConverters() as Set, KFunction>>) + clazz.getConverters() fun mapObject(value: U): Any? { val valueClazz: KClass<*> = value::class diff --git a/src/test/kotlin/com/mapk/kmapper/ConversionTest.kt b/src/test/kotlin/com/mapk/kmapper/ConversionTest.kt new file mode 100644 index 0000000..817bb1a --- /dev/null +++ b/src/test/kotlin/com/mapk/kmapper/ConversionTest.kt @@ -0,0 +1,137 @@ +package com.mapk.kmapper + +import com.mapk.conversion.AbstractKConverter +import com.mapk.conversion.KConvertBy +import java.lang.IllegalArgumentException +import java.math.BigDecimal +import java.math.BigInteger +import kotlin.reflect.KClass +import kotlin.reflect.jvm.jvmName +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.ValueSource + +@DisplayName("KConvertアノテーションによる変換のテスト") +class ConversionTest { + @Target(AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.RUNTIME) + @MustBeDocumented + @KConvertBy([FromString::class, FromNumber::class]) + annotation class ToNumber(val destination: KClass) + + class FromString(annotation: ToNumber) : AbstractKConverter(annotation) { + private val converter: (String) -> Number = when (annotation.destination) { + Double::class -> String::toDouble + Float::class -> String::toFloat + Long::class -> String::toLong + Int::class -> String::toInt + Short::class -> String::toShort + Byte::class -> String::toByte + BigDecimal::class -> { { BigDecimal(it) } } + BigInteger::class -> { { BigInteger(it) } } + else -> throw IllegalArgumentException("${annotation.destination.jvmName} is not supported.") + } + + override val srcClass = String::class + override fun convert(source: String?): Number? = source?.let(converter) + } + + class FromNumber(annotation: ToNumber) : AbstractKConverter(annotation) { + private val converter: (Number) -> Number = when (annotation.destination) { + Double::class -> Number::toDouble + Float::class -> Number::toFloat + Long::class -> Number::toLong + Int::class -> Number::toInt + Short::class -> Number::toShort + Byte::class -> Number::toByte + BigDecimal::class -> { { BigDecimal.valueOf(it.toDouble()) } } + BigInteger::class -> { { BigInteger.valueOf(it.toLong()) } } + else -> throw IllegalArgumentException("${annotation.destination.jvmName} is not supported.") + } + + override val srcClass = Number::class + override fun convert(source: Number?): Number? = source?.let(converter) + } + + data class Dst(@ToNumber(BigDecimal::class) val number: BigDecimal) + data class NumberSrc(val number: Number) + data class StringSrc(val number: String) + + enum class NumberSource(val values: Array) { + Doubles(arrayOf(1.0, -2.0, 3.5)), + Floats(arrayOf(4.1f, -5.09f, 6.00001f)), + Longs(arrayOf(7090, 800, 911)), + Ints(arrayOf(0, 123, 234)), + Shorts(arrayOf(365, 416, 511)), + Bytes(arrayOf(6, 7, 8)) + } + + @Nested + @DisplayName("KMapper") + inner class KMapperTest { + @ParameterizedTest + @EnumSource(NumberSource::class) + @DisplayName("Numberソース") + fun fromNumber(numbers: NumberSource) { + numbers.values.forEach { + val actual = KMapper(::Dst).map(NumberSrc(it)) + assertEquals(0, BigDecimal.valueOf(it.toDouble()).compareTo(actual.number)) + } + } + + @ParameterizedTest + @ValueSource(strings = ["100", "2.0", "-500"]) + @DisplayName("Stringソース") + fun fromString(str: String) { + val actual = KMapper(::Dst).map(StringSrc(str)) + assertEquals(0, BigDecimal(str).compareTo(actual.number)) + } + } + + @Nested + @DisplayName("PlainKMapper") + inner class PlainKMapperTest { + @ParameterizedTest + @EnumSource(NumberSource::class) + @DisplayName("Numberソース") + fun fromNumber(numbers: NumberSource) { + numbers.values.forEach { + val actual = PlainKMapper(::Dst).map(NumberSrc(it)) + assertEquals(0, BigDecimal.valueOf(it.toDouble()).compareTo(actual.number)) + } + } + + @ParameterizedTest + @ValueSource(strings = ["100", "2.0", "-500"]) + @DisplayName("Stringソース") + fun fromString(str: String) { + val actual = PlainKMapper(::Dst).map(StringSrc(str)) + assertEquals(0, BigDecimal(str).compareTo(actual.number)) + } + } + + @Nested + @DisplayName("BoundKMapper") + inner class BoundKMapperTest { + @ParameterizedTest + @EnumSource(NumberSource::class) + @DisplayName("Numberソース") + fun fromNumber(numbers: NumberSource) { + numbers.values.forEach { + val actual = BoundKMapper(::Dst, NumberSrc::class).map(NumberSrc(it)) + assertEquals(0, BigDecimal.valueOf(it.toDouble()).compareTo(actual.number)) + } + } + + @ParameterizedTest + @ValueSource(strings = ["100", "2.0", "-500"]) + @DisplayName("Stringソース") + fun fromString(str: String) { + val actual = BoundKMapper(::Dst, StringSrc::class).map(StringSrc(str)) + assertEquals(0, BigDecimal(str).compareTo(actual.number)) + } + } +}