diff --git a/build.gradle.kts b/build.gradle.kts index 7d2c579..2ec2853 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.mapk" -version = "0.20" +version = "0.21" java { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt b/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt index e61d775..03d0d96 100644 --- a/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt +++ b/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt @@ -21,9 +21,7 @@ internal class BoundParameterForMap(val param: KParameter, property: KP val paramClazz = param.type.classifier as KClass<*> val propertyClazz = property.returnType.classifier as KClass<*> - val converter = (convertersFromConstructors(paramClazz) + - convertersFromStaticMethods(paramClazz) + - convertersFromCompanionObject(paramClazz)) + val converter = 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/KMapper.kt b/src/main/kotlin/com/mapk/kmapper/KMapper.kt new file mode 100644 index 0000000..dc9461e --- /dev/null +++ b/src/main/kotlin/com/mapk/kmapper/KMapper.kt @@ -0,0 +1,110 @@ +package com.mapk.kmapper + +import com.mapk.annotations.KGetterAlias +import com.mapk.annotations.KGetterIgnore +import com.mapk.core.ArgumentBucket +import com.mapk.core.KFunctionForCall +import com.mapk.core.getAliasOrName +import com.mapk.core.isUseDefaultArgument +import com.mapk.core.toKConstructor +import java.lang.reflect.Method +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.KVisibility +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.javaGetter + +class KMapper private constructor( + private val function: KFunctionForCall, + parameterNameConverter: (String) -> String +) { + constructor(function: KFunction, parameterNameConverter: (String) -> String = { it }) : this( + KFunctionForCall(function), parameterNameConverter + ) + + constructor(clazz: KClass, parameterNameConverter: (String) -> String = { it }) : this( + clazz.toKConstructor(), parameterNameConverter + ) + + private val parameterMap: Map> = function.parameters + .filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() } + .associate { (parameterNameConverter(it.getAliasOrName()!!)) to ParameterForMap.newInstance(it) } + + private fun bindArguments(argumentBucket: ArgumentBucket, src: Any) { + src::class.memberProperties.forEach outer@{ property -> + // propertyが公開されていない場合は処理を行わない + if (property.visibility != KVisibility.PUBLIC) return@outer + + // ゲッターが取れない場合は処理を行わない + val javaGetter: Method = property.javaGetter ?: return@outer + + var alias: String? = null + // NOTE: IgnoreとAliasが同時に指定されるようなパターンを考慮してaliasが取れてもbreakしていない + javaGetter.annotations.forEach { + if (it is KGetterIgnore) return@outer // ignoreされている場合は処理を行わない + if (it is KGetterAlias) alias = it.value + } + + parameterMap[alias ?: property.name]?.let { + // javaGetterを呼び出す方が高速 + javaGetter.isAccessible = true + argumentBucket.putIfAbsent(it.param, javaGetter.invoke(src)?.let { value -> it.mapObject(value) }) + // 終了判定 + if (argumentBucket.isInitialized) return + } + } + } + + private fun bindArguments(argumentBucket: ArgumentBucket, src: Map<*, *>) { + src.forEach { (key, value) -> + parameterMap[key]?.let { param -> + // 取得した内容がnullでなければ適切にmapする + argumentBucket.putIfAbsent(param.param, value?.let { param.mapObject(value) }) + // 終了判定 + if (argumentBucket.isInitialized) return + } + } + } + + private fun bindArguments(argumentBucket: ArgumentBucket, srcPair: Pair<*, *>) { + parameterMap[srcPair.first.toString()]?.let { + argumentBucket.putIfAbsent(it.param, srcPair.second?.let { value -> it.mapObject(value) }) + } + } + + fun map(srcMap: Map): T { + val bucket: ArgumentBucket = function.getArgumentBucket() + bindArguments(bucket, srcMap) + + return function.call(bucket) + } + + fun map(srcPair: Pair): T { + val bucket: ArgumentBucket = function.getArgumentBucket() + bindArguments(bucket, srcPair) + + return function.call(bucket) + } + + fun map(src: Any): T { + val bucket: ArgumentBucket = function.getArgumentBucket() + bindArguments(bucket, src) + + return function.call(bucket) + } + + fun map(vararg args: Any): T { + val bucket: ArgumentBucket = function.getArgumentBucket() + + listOf(*args).forEach { arg -> + when (arg) { + is Map<*, *> -> bindArguments(bucket, arg) + is Pair<*, *> -> bindArguments(bucket, arg) + else -> bindArguments(bucket, arg) + } + } + + return function.call(bucket) + } +} diff --git a/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt b/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt new file mode 100644 index 0000000..4dd300e --- /dev/null +++ b/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt @@ -0,0 +1,50 @@ +package com.mapk.kmapper + +import com.mapk.core.EnumMapper +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.full.isSuperclassOf + +internal class ParameterForMap private constructor(val param: KParameter, private val clazz: KClass) { + private val javaClazz: Class by lazy { + clazz.java + } + // リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい + private val converters: Set, KFunction>> = clazz.getConverters() + + private val convertCache: MutableMap, (Any) -> Any?> = HashMap() + + fun mapObject(value: U): Any? { + val valueClazz: KClass<*> = value::class + + // 取得方法のキャッシュが有ればそれを用いる + convertCache[valueClazz]?.let { return it(value) } + + // パラメータに対してvalueが代入可能(同じもしくは親クラス)であればそのまま用いる + if (clazz.isSuperclassOf(valueClazz)) { + convertCache[valueClazz] = { value } + return value + } + + val converter: KFunction<*>? = converters.getConverter(valueClazz) + + val lambda: (Any) -> Any? = when { + // converterに一致する組み合わせが有れば設定されていればそれを使う + converter != null -> { { converter.call(it) } } + // 要求された値がenumかつ元が文字列ならenum mapperでマップ + javaClazz.isEnum && value is String -> { { EnumMapper.getEnum(javaClazz, it as String) } } + // 要求されているパラメータがStringならtoStringする + clazz == String::class -> { { it.toString() } } + else -> throw IllegalArgumentException("Can not convert $valueClazz to $clazz") + } + convertCache[valueClazz] = lambda + return lambda(value) + } + + companion object { + fun newInstance(param: KParameter): ParameterForMap<*> { + return ParameterForMap(param, param.type.classifier as KClass<*>) + } + } +} diff --git a/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt b/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt index 4e7dcb7..be1e830 100644 --- a/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt +++ b/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt @@ -6,10 +6,14 @@ import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.full.companionObjectInstance import kotlin.reflect.full.functions +import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.staticFunctions import kotlin.reflect.jvm.isAccessible -internal fun Collection>.getConverterMapFromFunctions(): Set, KFunction>> { +internal fun KClass.getConverters(): Set, KFunction>> = + convertersFromConstructors(this) + convertersFromStaticMethods(this) + convertersFromCompanionObject(this) + +private fun Collection>.getConverterMapFromFunctions(): Set, KFunction>> { return filter { it.annotations.any { annotation -> annotation is KConverter } } .map { func -> func.isAccessible = true @@ -18,19 +22,19 @@ internal fun Collection>.getConverterMapFromFunctions(): Set convertersFromConstructors(clazz: KClass): Set, KFunction>> { +private fun convertersFromConstructors(clazz: KClass): Set, KFunction>> { return clazz.constructors.getConverterMapFromFunctions() } @Suppress("UNCHECKED_CAST") -internal fun convertersFromStaticMethods(clazz: KClass): Set, KFunction>> { +private fun convertersFromStaticMethods(clazz: KClass): Set, KFunction>> { val staticFunctions: Collection> = clazz.staticFunctions as Collection> return staticFunctions.getConverterMapFromFunctions() } @Suppress("UNCHECKED_CAST") -internal fun convertersFromCompanionObject(clazz: KClass): Set, KFunction>> { +private fun convertersFromCompanionObject(clazz: KClass): Set, KFunction>> { return clazz.companionObjectInstance?.let { companionObject -> companionObject::class.functions .filter { it.annotations.any { annotation -> annotation is KConverter } } @@ -44,3 +48,7 @@ internal fun convertersFromCompanionObject(clazz: KClass): Set 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 9c95bb3..1c9a880 100644 --- a/src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt +++ b/src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt @@ -4,7 +4,6 @@ import com.mapk.core.EnumMapper import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.KParameter -import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSuperclassOf internal class PlainParameterForMap private constructor(val param: KParameter, private val clazz: KClass) { @@ -12,9 +11,7 @@ internal class PlainParameterForMap private constructor(val param: KPar clazz.java } // リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい - private val converters: Set, KFunction>> by lazy { - convertersFromConstructors(clazz) + convertersFromStaticMethods(clazz) + convertersFromCompanionObject(clazz) - } + private val converters: Set, KFunction>> = clazz.getConverters() fun mapObject(value: U): Any? { val valueClazz: KClass<*> = value::class @@ -22,7 +19,7 @@ internal class PlainParameterForMap private constructor(val param: KPar // パラメータに対してvalueが代入可能(同じもしくは親クラス)であればそのまま用いる if (clazz.isSuperclassOf(valueClazz)) return value - val converter: KFunction<*>? = getConverter(valueClazz) + val converter: KFunction<*>? = converters.getConverter(valueClazz) return when { // converterに一致する組み合わせが有れば設定されていればそれを使う @@ -35,10 +32,6 @@ internal class PlainParameterForMap private constructor(val param: KPar } } - // 引数の型がconverterに対して入力可能ならconverterを返す - private fun getConverter(input: KClass): KFunction? = - converters.find { (key, _) -> input.isSubclassOf(key) }?.second - companion object { fun newInstance(param: KParameter): PlainParameterForMap<*> { return PlainParameterForMap(param, param.type.classifier as KClass<*>) diff --git a/src/test/kotlin/com/mapk/kmapper/ConverterKMapperTest.kt b/src/test/kotlin/com/mapk/kmapper/ConverterKMapperTest.kt index c619be7..4f8465e 100644 --- a/src/test/kotlin/com/mapk/kmapper/ConverterKMapperTest.kt +++ b/src/test/kotlin/com/mapk/kmapper/ConverterKMapperTest.kt @@ -31,6 +31,37 @@ private data class BoundStaticMethodConverterSrc(val argument: String) @DisplayName("コンバータ有りでのマッピングテスト") class ConverterKMapperTest { + @Nested + @DisplayName("KMapper") + inner class KMapperTest { + @Test + @DisplayName("コンストラクターでのコンバートテスト") + fun constructorConverterTest() { + val mapper = KMapper(ConstructorConverterDst::class) + val result = mapper.map(mapOf("argument" to 1)) + + assertEquals(ConstructorConverter(1), result.argument) + } + + @Test + @DisplayName("コンパニオンオブジェクトに定義したコンバータでのコンバートテスト") + fun companionConverterTest() { + val mapper = KMapper(CompanionConverterDst::class) + val result = mapper.map(mapOf("argument" to "arg")) + + assertEquals("arg", result.argument.arg) + } + + @Test + @DisplayName("スタティックメソッドに定義したコンバータでのコンバートテスト") + fun staticMethodConverterTest() { + val mapper = KMapper(StaticMethodConverterDst::class) + val result = mapper.map(mapOf("argument" to "1,2,3")) + + assertTrue(intArrayOf(1, 2, 3) contentEquals result.argument.arg) + } + } + @Nested @DisplayName("PlainKMapper") inner class PlainKMapperTest { diff --git a/src/test/kotlin/com/mapk/kmapper/DefaultArgumentTest.kt b/src/test/kotlin/com/mapk/kmapper/DefaultArgumentTest.kt index 9720ac1..ae1a1df 100644 --- a/src/test/kotlin/com/mapk/kmapper/DefaultArgumentTest.kt +++ b/src/test/kotlin/com/mapk/kmapper/DefaultArgumentTest.kt @@ -13,6 +13,16 @@ class DefaultArgumentTest { private val src = Src(1, "src") + @Nested + @DisplayName("KMapper") + inner class KMapperTest { + @Test + fun test() { + val result = KMapper(::Dst).map(src) + assertEquals(Dst(1, "default"), result) + } + } + @Nested @DisplayName("PlainKMapper") inner class PlainKMapperTest { diff --git a/src/test/kotlin/com/mapk/kmapper/EnumMappingTest.kt b/src/test/kotlin/com/mapk/kmapper/EnumMappingTest.kt index 9515c3f..045d7a2 100644 --- a/src/test/kotlin/com/mapk/kmapper/EnumMappingTest.kt +++ b/src/test/kotlin/com/mapk/kmapper/EnumMappingTest.kt @@ -16,6 +16,20 @@ private class EnumMappingDst(val language: JvmLanguage?) @DisplayName("文字列 -> Enumのマッピングテスト") class EnumMappingTest { + @Nested + @DisplayName("KMapper") + inner class KMapperTest { + private val mapper = KMapper(EnumMappingDst::class) + + @ParameterizedTest(name = "Non-Null要求") + @EnumSource(value = JvmLanguage::class) + fun test(language: JvmLanguage) { + val result = mapper.map("language" to language.name) + + assertEquals(language, result.language) + } + } + @Nested @DisplayName("PlainKMapper") inner class PlainKMapperTest { diff --git a/src/test/kotlin/com/mapk/kmapper/KGetterIgnoreTest.kt b/src/test/kotlin/com/mapk/kmapper/KGetterIgnoreTest.kt index a0de4e8..85b68cd 100644 --- a/src/test/kotlin/com/mapk/kmapper/KGetterIgnoreTest.kt +++ b/src/test/kotlin/com/mapk/kmapper/KGetterIgnoreTest.kt @@ -13,6 +13,25 @@ class KGetterIgnoreTest { data class Dst(val arg1: Int, val arg2: String, val arg3: Int, val arg4: String) + @Nested + @DisplayName("KMapper") + inner class KMapperTest { + @Test + @DisplayName("フィールドを無視するテスト") + fun test() { + val src1 = Src1(1, "2-1", 31) + val src2 = Src2("2-2", 32, "4") + + val mapper = KMapper(::Dst) + + val dst1 = mapper.map(src1, src2) + val dst2 = mapper.map(src2, src1) + + assertTrue(dst1 == dst2) + assertEquals(Dst(1, "2-1", 32, "4"), dst1) + } + } + @Nested @DisplayName("PlainKMapper") inner class PlainKMapperTest { diff --git a/src/test/kotlin/com/mapk/kmapper/ParameterNameConverterTest.kt b/src/test/kotlin/com/mapk/kmapper/ParameterNameConverterTest.kt index b243ebd..ecac694 100644 --- a/src/test/kotlin/com/mapk/kmapper/ParameterNameConverterTest.kt +++ b/src/test/kotlin/com/mapk/kmapper/ParameterNameConverterTest.kt @@ -11,6 +11,24 @@ private data class BoundSrc(val camel_case: String) @DisplayName("パラメータ名変換のテスト") class ParameterNameConverterTest { + @Nested + @DisplayName("KMapper") + inner class KMapperTest { + @Test + @DisplayName("スネークケースsrc -> キャメルケースdst") + fun test() { + val expected = "snakeCase" + val src = mapOf("camel_case" to expected) + + val mapper = KMapper(CamelCaseDst::class) { + CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it) + } + val result = mapper.map(src) + + assertEquals(expected, result.camelCase) + } + } + @Nested @DisplayName("PlainKMapper") inner class PlainKMapperTest { diff --git a/src/test/kotlin/com/mapk/kmapper/PropertyAliasTest.kt b/src/test/kotlin/com/mapk/kmapper/PropertyAliasTest.kt index 23638b7..ec61bfd 100644 --- a/src/test/kotlin/com/mapk/kmapper/PropertyAliasTest.kt +++ b/src/test/kotlin/com/mapk/kmapper/PropertyAliasTest.kt @@ -20,6 +20,35 @@ private data class AliasedSrc( @DisplayName("エイリアスを貼った場合のテスト") class PropertyAliasTest { + @Nested + @DisplayName("KMapper") + inner class KMapperTest { + @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) + } + } + @Nested @DisplayName("PlainKMapper") inner class PlainKMapperTest { diff --git a/src/test/kotlin/com/mapk/kmapper/SimpleKMapperTest.kt b/src/test/kotlin/com/mapk/kmapper/SimpleKMapperTest.kt index 12a0169..a26ce59 100644 --- a/src/test/kotlin/com/mapk/kmapper/SimpleKMapperTest.kt +++ b/src/test/kotlin/com/mapk/kmapper/SimpleKMapperTest.kt @@ -73,6 +73,96 @@ class SimpleKMapperTest { return SimpleDst(arg1, arg2, arg3) } + @Nested + @DisplayName("KMapper") + inner class KMapperTest { + private val mappers: Set> = setOf( + KMapper(SimpleDst::class), + KMapper(::SimpleDst), + KMapper((SimpleDst)::factory), + KMapper(::instanceFunction), + KMapper(SimpleDstExt::class) + ) + + @Nested + @DisplayName("Mapからマップ") + inner class FromMap { + @Test + @DisplayName("Nullを含まない場合") + fun testWithoutNull() { + val srcMap: Map = mapOf( + "arg1" to 2, + "arg2" to "value", + "arg3" to 1.0 + ) + + val dsts = mappers.map { it.map(srcMap) } + + assertEquals(1, dsts.distinct().size) + dsts.first().let { + assertEquals(2, it.arg1) + assertEquals("value", it.arg2) + assertEquals(1.0, it.arg3) + } + } + + @Test + @DisplayName("Nullを含む場合") + fun testContainsNull() { + val srcMap: Map = mapOf( + "arg1" to 1, + "arg2" to null, + "arg3" to 2.0f + ) + + val dsts = mappers.map { it.map(srcMap) } + + assertEquals(1, dsts.distinct().size) + dsts.first().let { + assertEquals(1, it.arg1) + assertEquals(null, it.arg2) + assertEquals(2.0f, it.arg3) + } + } + } + + @Nested + @DisplayName("インスタンスからマップ") + inner class FromInstance { + @Test + @DisplayName("Nullを含まない場合") + fun testWithoutNull() { + val stringValue = "value" + + val src = Src1(stringValue) + + val dsts = mappers.map { it.map(src) } + + assertEquals(1, dsts.distinct().size) + dsts.first().let { + assertEquals(stringValue.length, it.arg1) + assertEquals(stringValue, it.arg2) + assertEquals(stringValue.length.toByte(), it.arg3) + } + } + + @Test + @DisplayName("Nullを含む場合") + fun testContainsNull() { + val src = Src1(null) + + val dsts = mappers.map { it.map(src) } + + assertEquals(1, dsts.distinct().size) + dsts.first().let { + assertEquals(0, it.arg1) + assertEquals(null, it.arg2) + assertEquals(0.toByte(), it.arg3) + } + } + } + } + @Nested @DisplayName("PlainKMapper") inner class PlainKMapperTest { diff --git a/src/test/kotlin/com/mapk/kmapper/StringMappingTest.kt b/src/test/kotlin/com/mapk/kmapper/StringMappingTest.kt index 0488639..ff42fe2 100644 --- a/src/test/kotlin/com/mapk/kmapper/StringMappingTest.kt +++ b/src/test/kotlin/com/mapk/kmapper/StringMappingTest.kt @@ -10,6 +10,16 @@ private data class BoundMappingSrc(val value: Int) @DisplayName("文字列に対してtoStringしたものを渡すテスト") class StringMappingTest { + @Nested + @DisplayName("KMapper") + inner class KMapperTest { + @Test + fun test() { + val result: StringMappingDst = KMapper(StringMappingDst::class).map("value" to 1) + assertEquals("1", result.value) + } + } + @Nested @DisplayName("PlainKMapper") inner class PlainKMapperTest {