diff --git a/build.gradle.kts b/build.gradle.kts index 166966d..cec3497 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.mapk" -version = "0.23" +version = "0.24" java { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt b/src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt index d9b6e7e..287c54f 100644 --- a/src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt +++ b/src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt @@ -45,7 +45,7 @@ class BoundKMapper private constructor( .filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() } .mapNotNull { val temp = srcPropertiesMap[parameterNameConverter(it.getAliasOrName()!!)]?.let { property -> - BoundParameterForMap.newInstance(it, property) + BoundParameterForMap.newInstance(it, property, parameterNameConverter) } // 必須引数に対応するプロパティがsrcに定義されていない場合エラー diff --git a/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt b/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt index 0f954ab..e3a3701 100644 --- a/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt +++ b/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt @@ -32,6 +32,23 @@ internal sealed class BoundParameterForMap { override fun map(src: S): Any? = converter.call(propertyGetter.invoke(src)) } + private class UseKMapper( + override val param: KParameter, + override val propertyGetter: Method, + private val kMapper: KMapper<*> + ) : BoundParameterForMap() { + // 1引数で呼び出すとMap/Pairが適切に処理されないため、2引数目にダミーを噛ませている + override fun map(src: S): Any? = kMapper.map(propertyGetter.invoke(src), PARAMETER_DUMMY) + } + + private class UseBoundKMapper( + override val param: KParameter, + override val propertyGetter: Method, + private val boundKMapper: BoundKMapper + ) : BoundParameterForMap() { + override fun map(src: S): Any? = boundKMapper.map(propertyGetter.invoke(src) as T) + } + private class ToEnum( override val param: KParameter, override val propertyGetter: Method, @@ -48,7 +65,11 @@ internal sealed class BoundParameterForMap { } companion object { - fun newInstance(param: KParameter, property: KProperty1): BoundParameterForMap { + fun newInstance( + param: KParameter, + property: KProperty1, + parameterNameConverter: (String) -> String + ): BoundParameterForMap { // ゲッターが無いならエラー val propertyGetter = property.javaGetter ?: throw IllegalArgumentException("${property.name} does not have getter.") @@ -77,7 +98,14 @@ internal sealed class BoundParameterForMap { return when { javaClazz.isEnum && propertyClazz == String::class -> ToEnum(param, propertyGetter, javaClazz) paramClazz == String::class -> ToString(param, propertyGetter) - else -> throw IllegalArgumentException("Can not convert $propertyClazz to $paramClazz") + // SrcがMapやPairならKMapperを使わないとマップできない + propertyClazz.isSubclassOf(Map::class) || propertyClazz.isSubclassOf(Pair::class) -> UseKMapper( + param, propertyGetter, KMapper(paramClazz, parameterNameConverter) + ) + // 何にも当てはまらなければBoundKMapperでマップを試みる + else -> UseBoundKMapper( + param, propertyGetter, BoundKMapper(paramClazz, propertyClazz, parameterNameConverter) + ) } } } diff --git a/src/main/kotlin/com/mapk/kmapper/KMapper.kt b/src/main/kotlin/com/mapk/kmapper/KMapper.kt index 4faff9a..037f8a2 100644 --- a/src/main/kotlin/com/mapk/kmapper/KMapper.kt +++ b/src/main/kotlin/com/mapk/kmapper/KMapper.kt @@ -31,7 +31,9 @@ class KMapper private constructor( private val parameterMap: Map> = function.parameters .filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() } - .associate { (parameterNameConverter(it.getAliasOrName()!!)) to ParameterForMap.newInstance(it) } + .associate { + (parameterNameConverter(it.getAliasOrName()!!)) to ParameterForMap.newInstance(it, parameterNameConverter) + } private val getCache: ConcurrentMap, List> = ConcurrentHashMap() diff --git a/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt b/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt index eceec68..92dc333 100644 --- a/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt +++ b/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt @@ -8,7 +8,11 @@ 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) { +internal class ParameterForMap private constructor( + val param: KParameter, + private val clazz: KClass, + private val parameterNameConverter: (String) -> String +) { private val javaClazz: Class by lazy { clazz.java } @@ -38,15 +42,18 @@ internal class ParameterForMap private constructor(val param: KParamete javaClazz.isEnum && value is String -> ParameterProcessor.ToEnum(javaClazz) // 要求されているパラメータがStringならtoStringする clazz == String::class -> ParameterProcessor.ToString - else -> throw IllegalArgumentException("Can not convert $valueClazz to $clazz") + // 入力がmapもしくはpairなら、KMapperを用いてマッピングを試みる + value is Map<*, *> || value is Pair<*, *> -> + ParameterProcessor.UseKMapper(KMapper(clazz, parameterNameConverter)) + else -> ParameterProcessor.UseBoundKMapper(BoundKMapper(clazz, valueClazz, parameterNameConverter)) } convertCache.putIfAbsent(valueClazz, processor) return processor.process(value) } companion object { - fun newInstance(param: KParameter): ParameterForMap<*> { - return ParameterForMap(param, param.type.classifier as KClass<*>) + fun newInstance(param: KParameter, parameterNameConverter: (String) -> String): ParameterForMap<*> { + return ParameterForMap(param, param.type.classifier as KClass<*>, parameterNameConverter) } } } @@ -62,6 +69,15 @@ private sealed class ParameterProcessor { override fun process(value: Any): Any? = converter.call(value) } + class UseKMapper(private val kMapper: KMapper<*>) : ParameterProcessor() { + override fun process(value: Any): Any? = kMapper.map(value, PARAMETER_DUMMY) + } + + @Suppress("UNCHECKED_CAST") + class UseBoundKMapper(private val boundKMapper: BoundKMapper) : ParameterProcessor() { + override fun process(value: Any): Any? = boundKMapper.map(value as T) + } + class ToEnum(private val javaClazz: Class<*>) : ParameterProcessor() { override fun process(value: Any): Any? = EnumMapper.getEnum(javaClazz, value as String) } diff --git a/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt b/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt index be1e830..4a109d4 100644 --- a/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt +++ b/src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt @@ -52,3 +52,6 @@ private fun convertersFromCompanionObject(clazz: KClass): Set Set, KFunction>>.getConverter(input: KClass): KFunction? = this.find { (key, _) -> input.isSubclassOf(key) }?.second + +// 再帰的マッピング時にKMapperでマップする場合、引数の数が1つだと正常にマッピングが機能しないため、2引数にするために用いるダミー +internal val PARAMETER_DUMMY = "" to null diff --git a/src/main/kotlin/com/mapk/kmapper/PlainKMapper.kt b/src/main/kotlin/com/mapk/kmapper/PlainKMapper.kt index 17bb1d9..871990d 100644 --- a/src/main/kotlin/com/mapk/kmapper/PlainKMapper.kt +++ b/src/main/kotlin/com/mapk/kmapper/PlainKMapper.kt @@ -29,7 +29,10 @@ class PlainKMapper private constructor( private val parameterMap: Map> = function.parameters .filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() } - .associate { (parameterNameConverter(it.getAliasOrName()!!)) to PlainParameterForMap.newInstance(it) } + .associate { + (parameterNameConverter(it.getAliasOrName()!!)) to + PlainParameterForMap.newInstance(it, parameterNameConverter) + } private fun bindArguments(argumentBucket: ArgumentBucket, src: Any) { src::class.memberProperties.forEach outer@{ property -> diff --git a/src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt b/src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt index 1c9a880..6273870 100644 --- a/src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt +++ b/src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt @@ -6,7 +6,11 @@ import kotlin.reflect.KFunction import kotlin.reflect.KParameter import kotlin.reflect.full.isSuperclassOf -internal class PlainParameterForMap private constructor(val param: KParameter, private val clazz: KClass) { +internal class PlainParameterForMap private constructor( + val param: KParameter, + private val clazz: KClass, + private val parameterNameConverter: (String) -> String +) { private val javaClazz: Class by lazy { clazz.java } @@ -28,13 +32,14 @@ internal class PlainParameterForMap private constructor(val param: KPar javaClazz.isEnum && value is String -> EnumMapper.getEnum(javaClazz, value) // 要求されているパラメータがStringならtoStringする clazz == String::class -> value.toString() - else -> throw IllegalArgumentException("Can not convert $valueClazz to $clazz") + // それ以外の場合PlainKMapperを作り再帰的なマッピングを試みる + else -> PlainKMapper(clazz, parameterNameConverter).map(value, PARAMETER_DUMMY) } } companion object { - fun newInstance(param: KParameter): PlainParameterForMap<*> { - return PlainParameterForMap(param, param.type.classifier as KClass<*>) + fun newInstance(param: KParameter, parameterNameConverter: (String) -> String): PlainParameterForMap<*> { + return PlainParameterForMap(param, param.type.classifier as KClass<*>, parameterNameConverter) } } } diff --git a/src/test/kotlin/com/mapk/kmapper/RecursiveMappingTest.kt b/src/test/kotlin/com/mapk/kmapper/RecursiveMappingTest.kt new file mode 100644 index 0000000..11166ce --- /dev/null +++ b/src/test/kotlin/com/mapk/kmapper/RecursiveMappingTest.kt @@ -0,0 +1,119 @@ +package com.mapk.kmapper + +import com.google.common.base.CaseFormat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@DisplayName("再帰的マッピングのテスト") +class RecursiveMappingTest { + private data class InnerSrc( + val hogeHoge: Int, + val fugaFuga: Short, + val piyoPiyo: String, + val mogeMoge: Pair + ) + private data class InnerSnakeSrc( + val hoge_hoge: Int, + val fuga_fuga: Short, + val piyo_piyo: String, + val moge_moge: Pair + ) + + private data class InnerInnerDst(val poiPoi: Int?) + private data class InnerDst(val hogeHoge: Int, val piyoPiyo: String, val mogeMoge: InnerInnerDst) + + private data class Src(val fooFoo: InnerSrc, val barBar: Boolean, val bazBaz: Int) + private data class SnakeSrc(val foo_foo: InnerSnakeSrc, val bar_bar: Boolean, val baz_baz: Int) + private data class MapSrc(val fooFoo: Map, val barBar: Boolean, val bazBaz: Int) + private data class Dst(val fooFoo: InnerDst, val bazBaz: Int) + + companion object { + private val src = Src(InnerSrc(1, 2, "three", "poiPoi" to 5), true, 4) + private val snakeSrc = SnakeSrc(InnerSnakeSrc(1, 2, "three", "poi_poi" to 5), true, 4) + private val mapSrc = MapSrc(mapOf("hogeHoge" to 1, "piyoPiyo" to "three", "mogeMoge" to ("poiPoi" to 5)), true, 4) + private val expected = Dst(InnerDst(1, "three", InnerInnerDst(5)), 4) + } + + @Nested + @DisplayName("KMapper") + inner class KMapperTest { + @Test + @DisplayName("シンプルなマッピング") + fun test() { + val actual = KMapper(::Dst).map(src) + assertEquals(expected, actual) + } + + @Test + @DisplayName("スネークケースsrc -> キャメルケースdst") + fun snakeToCamel() { + val actual = KMapper(::Dst) { + CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it) + }.map(snakeSrc) + assertEquals(expected, actual) + } + + @Test + @DisplayName("内部フィールドがMapの場合") + fun includesMap() { + val actual = KMapper(::Dst).map(mapSrc) + assertEquals(expected, actual) + } + } + + @Nested + @DisplayName("PlainKMapper") + inner class PlainKMapperTest { + @Test + @DisplayName("シンプルなマッピング") + fun test() { + val actual = PlainKMapper(::Dst).map(src) + assertEquals(expected, actual) + } + + @Test + @DisplayName("スネークケースsrc -> キャメルケースdst") + fun snakeToCamel() { + val actual = PlainKMapper(::Dst) { + CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it) + }.map(snakeSrc) + assertEquals(expected, actual) + } + + @Test + @DisplayName("内部フィールドがMapの場合") + fun includesMap() { + val actual = PlainKMapper(::Dst).map(mapSrc) + assertEquals(expected, actual) + } + } + + @Nested + @DisplayName("BoundKMapper") + inner class BoundKMapperTest { + @Test + @DisplayName("シンプルなマッピング") + fun test() { + val actual = BoundKMapper(::Dst, Src::class).map(src) + assertEquals(expected, actual) + } + + @Test + @DisplayName("スネークケースsrc -> キャメルケースdst") + fun snakeToCamel() { + val actual = BoundKMapper(::Dst, SnakeSrc::class) { + CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it) + }.map(snakeSrc) + assertEquals(expected, actual) + } + + @Test + @DisplayName("内部フィールドがMapの場合") + fun includesMap() { + val actual = BoundKMapper(::Dst, MapSrc::class).map(mapSrc) + assertEquals(expected, actual) + } + } +}