diff --git a/README.md b/README.md index 0eb326f..081984c 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ class Dst( ### Set alias on map #### for getter ```kotlin -class Src(@get:PropertyAlias("aliased") val str: String) +class Src(@KGetterAlias("aliased") val str: String) class Dst(val aliased: String) ``` @@ -53,7 +53,7 @@ class Dst(val aliased: String) ```kotlin class Src(val str: String) -class Dst(@param:PropertyAlias("str") private val _src: String) { +class Dst(@param:KPropertyAlias("str") private val _src: String) { val src = _src.someArrangement } ``` @@ -64,7 +64,7 @@ val srcMap = mapOf("snake_case" to "SnakeCase") class Dst(val snakeCase: String) -val dst: Dst = Mapper(DataClass::class.primaryConstructor!!) { it.toSnakeCase }.map(src) +val dst: Dst = Mapper(::DataClass) { it.toSnakeCase }.map(src) ``` ### Map param to another class @@ -72,7 +72,7 @@ val dst: Dst = Mapper(DataClass::class.primaryConstructor!!) { it.toSnakeCase }. ```kotlin class CreatorClass @SingleArgCreator constructor(val arg: String) { companion object { - @SingleArgCreator + @KConverter fun fromInt(arg: Int): CreatorClass { return CreatorClass(arg.toString) } diff --git a/src/main/kotlin/com/wrongwrong/mapk/annotations/KGetterAlias.kt b/src/main/kotlin/com/wrongwrong/mapk/annotations/KGetterAlias.kt new file mode 100644 index 0000000..6bf6815 --- /dev/null +++ b/src/main/kotlin/com/wrongwrong/mapk/annotations/KGetterAlias.kt @@ -0,0 +1,5 @@ +package com.wrongwrong.mapk.annotations + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class KGetterAlias(val value: String) diff --git a/src/main/kotlin/com/wrongwrong/mapk/annotations/KPropertyAlias.kt b/src/main/kotlin/com/wrongwrong/mapk/annotations/KPropertyAlias.kt index 5ada366..eab10fa 100644 --- a/src/main/kotlin/com/wrongwrong/mapk/annotations/KPropertyAlias.kt +++ b/src/main/kotlin/com/wrongwrong/mapk/annotations/KPropertyAlias.kt @@ -1,5 +1,5 @@ package com.wrongwrong.mapk.annotations -@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.PROPERTY_GETTER) +@Target(AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) annotation class KPropertyAlias(val value: String) diff --git a/src/main/kotlin/com/wrongwrong/mapk/core/KFunctionForCall.kt b/src/main/kotlin/com/wrongwrong/mapk/core/KFunctionForCall.kt new file mode 100644 index 0000000..97fdab6 --- /dev/null +++ b/src/main/kotlin/com/wrongwrong/mapk/core/KFunctionForCall.kt @@ -0,0 +1,25 @@ +package com.wrongwrong.mapk.core + +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.jvm.isAccessible + +class KFunctionForCall(private val function: KFunction, instance: Any? = null) { + val parameters: List = function.parameters + private val originalArray: Array + val argumentArray: Array get() = originalArray.copyOf() + + init { + // この関数には確実にアクセスするためアクセシビリティ書き換え + function.isAccessible = true + originalArray = if (instance != null) { + Array(parameters.size) { if (it == 0) instance else null } + } else { + Array(parameters.size) { null } + } + } + + fun call(arguments: Array): T { + return function.call(*arguments) + } +} diff --git a/src/main/kotlin/com/wrongwrong/mapk/core/KMapper.kt b/src/main/kotlin/com/wrongwrong/mapk/core/KMapper.kt index 1083db6..9a1934c 100644 --- a/src/main/kotlin/com/wrongwrong/mapk/core/KMapper.kt +++ b/src/main/kotlin/com/wrongwrong/mapk/core/KMapper.kt @@ -1,132 +1,122 @@ package com.wrongwrong.mapk.core import com.wrongwrong.mapk.annotations.KConstructor +import com.wrongwrong.mapk.annotations.KGetterAlias import com.wrongwrong.mapk.annotations.KPropertyAlias import com.wrongwrong.mapk.annotations.KPropertyIgnore +import java.lang.reflect.Method import kotlin.reflect.KClass import kotlin.reflect.KFunction -import kotlin.reflect.KProperty1 +import kotlin.reflect.KParameter import kotlin.reflect.KVisibility import kotlin.reflect.full.companionObjectInstance +import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.functions import kotlin.reflect.full.isSuperclassOf import kotlin.reflect.full.memberProperties import kotlin.reflect.full.primaryConstructor -import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.javaGetter + +class KMapper private constructor( + private val function: KFunctionForCall, + propertyNameConverter: (String) -> String = { it } +) { + constructor(function: KFunction, propertyNameConverter: (String) -> String = { it }) : this( + KFunctionForCall(function), propertyNameConverter + ) -class KMapper(private val function: KFunction, propertyNameConverter: (String) -> String = { it }) { constructor(clazz: KClass, propertyNameConverter: (String) -> String = { it }) : this( getTarget(clazz), propertyNameConverter ) - private val parameters: Set> = function.parameters - .map { ParameterForMap.newInstance(it, propertyNameConverter) } - .toSet() + private val parameterMap: Map> = function.parameters + .filter { it.kind != KParameter.Kind.INSTANCE } + .associate { + (it.findAnnotation()?.value ?: propertyNameConverter(it.name!!)) to + ParameterForMap.newInstance(it) + } init { - if (parameters.isEmpty()) throw IllegalArgumentException("This function is not require arguments.") - - // private関数に対してもマッピングできなければ何かと不都合があるため、accessibleは書き換える - function.isAccessible = true + if (parameterMap.isEmpty()) throw IllegalArgumentException("This function is not require arguments.") } - fun map(srcMap: Map): T { - return parameters.associate { - // 取得した内容がnullでなければ適切にmapする - it.param to srcMap.getValue(it.name)?.let { value -> - mapObject(it, value) + private fun bindParameters(targetArray: Array, src: Any) { + src::class.memberProperties.forEach { property -> + val javaGetter: Method? = property.javaGetter + if (javaGetter != null && property.visibility == KVisibility.PUBLIC && property.annotations.none { annotation -> annotation is KPropertyIgnore }) { + parameterMap[property.findAnnotation()?.value ?: property.name]?.let { + // javaGetterを呼び出す方が高速 + javaGetter.isAccessible = true + targetArray[it.index] = javaGetter.invoke(src)?.let { value -> mapObject(it, value) } + } } - }.let { function.callBy(it) } + } } - fun map(srcPair: Pair): T = parameters - .single { it.name == srcPair.first } - .let { - function.callBy(mapOf(it.param to srcPair.second?.let { value -> mapObject(it, value) })) + private fun bindParameters(targetArray: Array, src: Map<*, *>) { + src.forEach { (key, value) -> + parameterMap[key]?.let { param -> + // 取得した内容がnullでなければ適切にmapする + targetArray[param.index] = value?.let { mapObject(param, it) } + } } + } - fun map(src: Any): T { - val srcMap: Map> = - src::class.memberProperties.filterTargets().associate { property -> - val getter = property.getAccessibleGetter() + private fun bindParameters(targetArray: Array, srcPair: Pair<*, *>) { + parameterMap.getValue(srcPair.first.toString()).let { + targetArray[it.index] = srcPair.second?.let { value -> mapObject(it, value) } + } + } - val key = getter.annotations - .find { it is KPropertyAlias } - ?.let { (it as KPropertyAlias).value } - ?: property.name + fun map(srcMap: Map): T { + val array: Array = function.argumentArray + bindParameters(array, srcMap) + return function.call(array) + } - key to getter - } + fun map(srcPair: Pair): T { + val array: Array = function.argumentArray + bindParameters(array, srcPair) + return function.call(array) + } - return parameters.associate { - // 取得した内容がnullでなければ適切にmapする - it.param to srcMap.getValue(it.name).call(src)?.let { value -> - mapObject(it, value) - } - }.let { function.callBy(it) } + fun map(src: Any): T { + val array: Array = function.argumentArray + bindParameters(array, src) + return function.call(array) } fun map(vararg args: Any): T { - val srcMap: Map Any?> = listOf(*args) - .map { arg -> - when (arg) { - is Map<*, *> -> arg.entries.associate { (key, value) -> - (key as String) to { value } - } - is Pair<*, *> -> mapOf(arg.first as String to { arg.second }) - else -> { - arg::class.memberProperties.filterTargets().associate { property -> - val getter = property.getAccessibleGetter() - - val key = getter.annotations - .find { it is KPropertyAlias } - ?.let { (it as KPropertyAlias).value } - ?: property.name - - key to { getter.call(arg) } - } - } - } - }.reduce { acc, map -> - acc + map - } + val array: Array = function.argumentArray - return parameters.associate { - // 取得した内容がnullでなければ適切にmapする - it.param to srcMap.getValue(it.name)()?.let { value -> - mapObject(it, value) + listOf(*args).forEach { arg -> + when (arg) { + is Map<*, *> -> bindParameters(array, arg) + is Pair<*, *> -> bindParameters(array, arg) + else -> bindParameters(array, arg) } - }.let { function.callBy(it) } - } -} + } -private fun Collection>.filterTargets(): Collection> { - return filter { - it.visibility == KVisibility.PUBLIC && it.annotations.none { annotation -> annotation is KPropertyIgnore } + return function.call(array) } } -private fun KProperty1<*, *>.getAccessibleGetter(): KProperty1.Getter<*, *> { - // アクセス制限の有るクラスではpublicなプロパティでもゲッターにアクセスできない場合が有るため、アクセス可能にして使う - getter.isAccessible = true - return getter -} - @Suppress("UNCHECKED_CAST") -internal fun getTarget(clazz: KClass): KFunction { - val factoryConstructor: List> = +internal fun getTarget(clazz: KClass): KFunctionForCall { + val factoryConstructor: List> = clazz.companionObjectInstance?.let { companionObject -> companionObject::class.functions .filter { it.annotations.any { annotation -> annotation is KConstructor } } - .map { KFunctionWithInstance(it, companionObject) as KFunction } + .map { KFunctionForCall(it, companionObject) as KFunctionForCall } } ?: emptyList() - val constructors: List> = factoryConstructor + clazz.constructors + val constructors: List> = factoryConstructor + clazz.constructors .filter { it.annotations.any { annotation -> annotation is KConstructor } } + .map { KFunctionForCall(it) } if (constructors.size == 1) return constructors.single() - if (constructors.isEmpty()) return clazz.primaryConstructor!! + if (constructors.isEmpty()) return KFunctionForCall(clazz.primaryConstructor!!) throw IllegalArgumentException("Find multiple target.") } diff --git a/src/main/kotlin/com/wrongwrong/mapk/core/ParameterForMap.kt b/src/main/kotlin/com/wrongwrong/mapk/core/ParameterForMap.kt index 8df3d96..4f3d0b3 100644 --- a/src/main/kotlin/com/wrongwrong/mapk/core/ParameterForMap.kt +++ b/src/main/kotlin/com/wrongwrong/mapk/core/ParameterForMap.kt @@ -1,7 +1,6 @@ package com.wrongwrong.mapk.core import com.wrongwrong.mapk.annotations.KConverter -import com.wrongwrong.mapk.annotations.KPropertyAlias import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.KParameter @@ -11,16 +10,7 @@ import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.staticFunctions import kotlin.reflect.jvm.isAccessible -internal class ParameterForMap private constructor( - val param: KParameter, - val clazz: KClass, - propertyNameConverter: (String) -> String -) { - val name: String = param.annotations - .find { it is KPropertyAlias } - ?.let { (it as KPropertyAlias).value } - ?: propertyNameConverter(param.name!!) - +internal class ParameterForMap private constructor(val index: Int, val clazz: KClass) { val javaClazz: Class by lazy { clazz.java } @@ -34,8 +24,8 @@ internal class ParameterForMap private constructor( creators.find { (key, _) -> input.isSubclassOf(key) }?.second companion object { - fun newInstance(param: KParameter, propertyNameConverter: (String) -> String): ParameterForMap<*> { - return ParameterForMap(param, param.type.classifier as KClass<*>, propertyNameConverter) + fun newInstance(param: KParameter): ParameterForMap<*> { + return ParameterForMap(param.index, param.type.classifier as KClass<*>) } } } diff --git a/src/test/kotlin/mapk/core/GetTargetTest.kt b/src/test/kotlin/mapk/core/GetTargetTest.kt index 7214d5e..54057a0 100644 --- a/src/test/kotlin/mapk/core/GetTargetTest.kt +++ b/src/test/kotlin/mapk/core/GetTargetTest.kt @@ -3,8 +3,12 @@ package mapk.core import com.wrongwrong.mapk.annotations.KConstructor +import com.wrongwrong.mapk.core.KFunctionForCall import com.wrongwrong.mapk.core.getTarget +import kotlin.reflect.KFunction +import kotlin.reflect.full.memberProperties import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.isAccessible import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.DisplayName @@ -27,26 +31,34 @@ class MultipleConstructorDst @KConstructor constructor(val argument: Int) { @KConstructor constructor(argument: String) : this(argument.toInt()) } +@Suppress("UNCHECKED_CAST") @DisplayName("クラスからのコンストラクタ抽出関連テスト") class GetTargetTest { + private fun KFunctionForCall.getTargetFunction(): KFunction { + return this::class.memberProperties.first { it.name == "function" }.getter.let { + it.isAccessible = true + it.call(this) as KFunction + } + } + @Test @DisplayName("セカンダリコンストラクタからの取得テスト") fun testGetFromSecondaryConstructor() { - val function = getTarget(SecondaryConstructorDst::class) + val function = getTarget(SecondaryConstructorDst::class).getTargetFunction() assertTrue(function.annotations.any { it is KConstructor }) } @Test @DisplayName("ファクトリーメソッドからの取得テスト") fun testGetFromFactoryMethod() { - val function = getTarget(CompanionFactoryDst::class) + val function = getTarget(SecondaryConstructorDst::class).getTargetFunction() assertTrue(function.annotations.any { it is KConstructor }) } @Test @DisplayName("無指定でプライマリコンストラクタからの取得テスト") fun testGetFromPrimaryConstructor() { - val function = getTarget(ConstructorDst::class) + val function = getTarget(ConstructorDst::class).getTargetFunction() assertEquals(ConstructorDst::class.primaryConstructor, function) } diff --git a/src/test/kotlin/mapk/core/ParamAliasTest.kt b/src/test/kotlin/mapk/core/ParamAliasTest.kt new file mode 100644 index 0000000..3929b90 --- /dev/null +++ b/src/test/kotlin/mapk/core/ParamAliasTest.kt @@ -0,0 +1,47 @@ +package mapk.core + +import com.wrongwrong.mapk.annotations.KGetterAlias +import com.wrongwrong.mapk.annotations.KPropertyAlias +import com.wrongwrong.mapk.core.KMapper +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +private data class AliasedDst( + val arg1: Double, + @param:KPropertyAlias("arg3") val arg2: Int +) + +private data class AliasedSrc( + @KGetterAlias("arg1") + val arg2: Double, + val arg3: Int +) + +@DisplayName("エイリアスを貼った場合のテスト") +class ParamAliasTest { + @Test + @DisplayName("パラメータにエイリアスを貼った場合") + fun paramAliasTest() { + val src = mapOf( + "arg1" to 1.0, + "arg2" to "2", + "arg3" to 3 + ) + + val result = KMapper(::AliasedDst).map(src) + + assertEquals(1.0, result.arg1) + assertEquals(3, result.arg2) + } + + @Test + @DisplayName("ゲッターにエイリアスを貼った場合") + fun getAliasTest() { + val src = AliasedSrc(1.0, 2) + val result = KMapper(::AliasedDst).map(src) + + assertEquals(1.0, result.arg1) + assertEquals(2, result.arg2) + } +} diff --git a/src/test/kotlin/mapk/core/SimpleKMapperTest.kt b/src/test/kotlin/mapk/core/SimpleKMapperTest.kt index d63caa3..d90be59 100644 --- a/src/test/kotlin/mapk/core/SimpleKMapperTest.kt +++ b/src/test/kotlin/mapk/core/SimpleKMapperTest.kt @@ -6,7 +6,6 @@ import com.wrongwrong.mapk.annotations.KConstructor import com.wrongwrong.mapk.core.KMapper import java.math.BigInteger import kotlin.reflect.full.isSubclassOf -import kotlin.reflect.full.primaryConstructor import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested @@ -71,7 +70,7 @@ class SimpleKMapperTest { private val mappers: Set> = setOf( KMapper(SimpleDst::class), - KMapper(SimpleDst::class.primaryConstructor!!), + KMapper(::SimpleDst), KMapper((SimpleDst)::factory), KMapper(this::instanceFunction), KMapper(SimpleDstExt::class)