Skip to content
This repository was archived by the owner on Jan 20, 2023. It is now read-only.

Commit 4518692

Browse files
authored
Merge pull request #27 from k163377/feature
Added support for recursive mapping.
2 parents 7f4ff40 + 0c580b7 commit 4518692

File tree

9 files changed

+190
-14
lines changed

9 files changed

+190
-14
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
group = "com.mapk"
9-
version = "0.23"
9+
version = "0.24"
1010

1111
java {
1212
sourceCompatibility = JavaVersion.VERSION_1_8

src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class BoundKMapper<S : Any, D : Any> private constructor(
4545
.filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() }
4646
.mapNotNull {
4747
val temp = srcPropertiesMap[parameterNameConverter(it.getAliasOrName()!!)]?.let { property ->
48-
BoundParameterForMap.newInstance(it, property)
48+
BoundParameterForMap.newInstance(it, property, parameterNameConverter)
4949
}
5050

5151
// 必須引数に対応するプロパティがsrcに定義されていない場合エラー

src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@ internal sealed class BoundParameterForMap<S> {
3232
override fun map(src: S): Any? = converter.call(propertyGetter.invoke(src))
3333
}
3434

35+
private class UseKMapper<S : Any>(
36+
override val param: KParameter,
37+
override val propertyGetter: Method,
38+
private val kMapper: KMapper<*>
39+
) : BoundParameterForMap<S>() {
40+
// 1引数で呼び出すとMap/Pairが適切に処理されないため、2引数目にダミーを噛ませている
41+
override fun map(src: S): Any? = kMapper.map(propertyGetter.invoke(src), PARAMETER_DUMMY)
42+
}
43+
44+
private class UseBoundKMapper<S : Any, T : Any>(
45+
override val param: KParameter,
46+
override val propertyGetter: Method,
47+
private val boundKMapper: BoundKMapper<T, *>
48+
) : BoundParameterForMap<S>() {
49+
override fun map(src: S): Any? = boundKMapper.map(propertyGetter.invoke(src) as T)
50+
}
51+
3552
private class ToEnum<S : Any>(
3653
override val param: KParameter,
3754
override val propertyGetter: Method,
@@ -48,7 +65,11 @@ internal sealed class BoundParameterForMap<S> {
4865
}
4966

5067
companion object {
51-
fun <S : Any> newInstance(param: KParameter, property: KProperty1<S, *>): BoundParameterForMap<S> {
68+
fun <S : Any> newInstance(
69+
param: KParameter,
70+
property: KProperty1<S, *>,
71+
parameterNameConverter: (String) -> String
72+
): BoundParameterForMap<S> {
5273
// ゲッターが無いならエラー
5374
val propertyGetter = property.javaGetter
5475
?: throw IllegalArgumentException("${property.name} does not have getter.")
@@ -77,7 +98,14 @@ internal sealed class BoundParameterForMap<S> {
7798
return when {
7899
javaClazz.isEnum && propertyClazz == String::class -> ToEnum(param, propertyGetter, javaClazz)
79100
paramClazz == String::class -> ToString(param, propertyGetter)
80-
else -> throw IllegalArgumentException("Can not convert $propertyClazz to $paramClazz")
101+
// SrcがMapやPairならKMapperを使わないとマップできない
102+
propertyClazz.isSubclassOf(Map::class) || propertyClazz.isSubclassOf(Pair::class) -> UseKMapper(
103+
param, propertyGetter, KMapper(paramClazz, parameterNameConverter)
104+
)
105+
// 何にも当てはまらなければBoundKMapperでマップを試みる
106+
else -> UseBoundKMapper(
107+
param, propertyGetter, BoundKMapper(paramClazz, propertyClazz, parameterNameConverter)
108+
)
81109
}
82110
}
83111
}

src/main/kotlin/com/mapk/kmapper/KMapper.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ class KMapper<T : Any> private constructor(
3131

3232
private val parameterMap: Map<String, ParameterForMap<*>> = function.parameters
3333
.filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() }
34-
.associate { (parameterNameConverter(it.getAliasOrName()!!)) to ParameterForMap.newInstance(it) }
34+
.associate {
35+
(parameterNameConverter(it.getAliasOrName()!!)) to ParameterForMap.newInstance(it, parameterNameConverter)
36+
}
3537

3638
private val getCache: ConcurrentMap<KClass<*>, List<ArgumentBinder>> = ConcurrentHashMap()
3739

src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import kotlin.reflect.KFunction
88
import kotlin.reflect.KParameter
99
import kotlin.reflect.full.isSuperclassOf
1010

11-
internal class ParameterForMap<T : Any> private constructor(val param: KParameter, private val clazz: KClass<T>) {
11+
internal class ParameterForMap<T : Any> private constructor(
12+
val param: KParameter,
13+
private val clazz: KClass<T>,
14+
private val parameterNameConverter: (String) -> String
15+
) {
1216
private val javaClazz: Class<T> by lazy {
1317
clazz.java
1418
}
@@ -38,15 +42,18 @@ internal class ParameterForMap<T : Any> private constructor(val param: KParamete
3842
javaClazz.isEnum && value is String -> ParameterProcessor.ToEnum(javaClazz)
3943
// 要求されているパラメータがStringならtoStringする
4044
clazz == String::class -> ParameterProcessor.ToString
41-
else -> throw IllegalArgumentException("Can not convert $valueClazz to $clazz")
45+
// 入力がmapもしくはpairなら、KMapperを用いてマッピングを試みる
46+
value is Map<*, *> || value is Pair<*, *> ->
47+
ParameterProcessor.UseKMapper(KMapper(clazz, parameterNameConverter))
48+
else -> ParameterProcessor.UseBoundKMapper(BoundKMapper(clazz, valueClazz, parameterNameConverter))
4249
}
4350
convertCache.putIfAbsent(valueClazz, processor)
4451
return processor.process(value)
4552
}
4653

4754
companion object {
48-
fun newInstance(param: KParameter): ParameterForMap<*> {
49-
return ParameterForMap(param, param.type.classifier as KClass<*>)
55+
fun newInstance(param: KParameter, parameterNameConverter: (String) -> String): ParameterForMap<*> {
56+
return ParameterForMap(param, param.type.classifier as KClass<*>, parameterNameConverter)
5057
}
5158
}
5259
}
@@ -62,6 +69,15 @@ private sealed class ParameterProcessor {
6269
override fun process(value: Any): Any? = converter.call(value)
6370
}
6471

72+
class UseKMapper(private val kMapper: KMapper<*>) : ParameterProcessor() {
73+
override fun process(value: Any): Any? = kMapper.map(value, PARAMETER_DUMMY)
74+
}
75+
76+
@Suppress("UNCHECKED_CAST")
77+
class UseBoundKMapper<T : Any>(private val boundKMapper: BoundKMapper<T, *>) : ParameterProcessor() {
78+
override fun process(value: Any): Any? = boundKMapper.map(value as T)
79+
}
80+
6581
class ToEnum(private val javaClazz: Class<*>) : ParameterProcessor() {
6682
override fun process(value: Any): Any? = EnumMapper.getEnum(javaClazz, value as String)
6783
}

src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,6 @@ private fun <T : Any> convertersFromCompanionObject(clazz: KClass<T>): Set<Pair<
5252
// 引数の型がconverterに対して入力可能ならconverterを返す
5353
internal fun <T : Any> Set<Pair<KClass<*>, KFunction<T>>>.getConverter(input: KClass<out T>): KFunction<T>? =
5454
this.find { (key, _) -> input.isSubclassOf(key) }?.second
55+
56+
// 再帰的マッピング時にKMapperでマップする場合、引数の数が1つだと正常にマッピングが機能しないため、2引数にするために用いるダミー
57+
internal val PARAMETER_DUMMY = "" to null

src/main/kotlin/com/mapk/kmapper/PlainKMapper.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ class PlainKMapper<T : Any> private constructor(
2929

3030
private val parameterMap: Map<String, PlainParameterForMap<*>> = function.parameters
3131
.filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() }
32-
.associate { (parameterNameConverter(it.getAliasOrName()!!)) to PlainParameterForMap.newInstance(it) }
32+
.associate {
33+
(parameterNameConverter(it.getAliasOrName()!!)) to
34+
PlainParameterForMap.newInstance(it, parameterNameConverter)
35+
}
3336

3437
private fun bindArguments(argumentBucket: ArgumentBucket, src: Any) {
3538
src::class.memberProperties.forEach outer@{ property ->

src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import kotlin.reflect.KFunction
66
import kotlin.reflect.KParameter
77
import kotlin.reflect.full.isSuperclassOf
88

9-
internal class PlainParameterForMap<T : Any> private constructor(val param: KParameter, private val clazz: KClass<T>) {
9+
internal class PlainParameterForMap<T : Any> private constructor(
10+
val param: KParameter,
11+
private val clazz: KClass<T>,
12+
private val parameterNameConverter: (String) -> String
13+
) {
1014
private val javaClazz: Class<T> by lazy {
1115
clazz.java
1216
}
@@ -28,13 +32,14 @@ internal class PlainParameterForMap<T : Any> private constructor(val param: KPar
2832
javaClazz.isEnum && value is String -> EnumMapper.getEnum(javaClazz, value)
2933
// 要求されているパラメータがStringならtoStringする
3034
clazz == String::class -> value.toString()
31-
else -> throw IllegalArgumentException("Can not convert $valueClazz to $clazz")
35+
// それ以外の場合PlainKMapperを作り再帰的なマッピングを試みる
36+
else -> PlainKMapper(clazz, parameterNameConverter).map(value, PARAMETER_DUMMY)
3237
}
3338
}
3439

3540
companion object {
36-
fun newInstance(param: KParameter): PlainParameterForMap<*> {
37-
return PlainParameterForMap(param, param.type.classifier as KClass<*>)
41+
fun newInstance(param: KParameter, parameterNameConverter: (String) -> String): PlainParameterForMap<*> {
42+
return PlainParameterForMap(param, param.type.classifier as KClass<*>, parameterNameConverter)
3843
}
3944
}
4045
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.mapk.kmapper
2+
3+
import com.google.common.base.CaseFormat
4+
import org.junit.jupiter.api.Assertions.assertEquals
5+
import org.junit.jupiter.api.DisplayName
6+
import org.junit.jupiter.api.Nested
7+
import org.junit.jupiter.api.Test
8+
9+
@DisplayName("再帰的マッピングのテスト")
10+
class RecursiveMappingTest {
11+
private data class InnerSrc(
12+
val hogeHoge: Int,
13+
val fugaFuga: Short,
14+
val piyoPiyo: String,
15+
val mogeMoge: Pair<String, Int>
16+
)
17+
private data class InnerSnakeSrc(
18+
val hoge_hoge: Int,
19+
val fuga_fuga: Short,
20+
val piyo_piyo: String,
21+
val moge_moge: Pair<String, Int>
22+
)
23+
24+
private data class InnerInnerDst(val poiPoi: Int?)
25+
private data class InnerDst(val hogeHoge: Int, val piyoPiyo: String, val mogeMoge: InnerInnerDst)
26+
27+
private data class Src(val fooFoo: InnerSrc, val barBar: Boolean, val bazBaz: Int)
28+
private data class SnakeSrc(val foo_foo: InnerSnakeSrc, val bar_bar: Boolean, val baz_baz: Int)
29+
private data class MapSrc(val fooFoo: Map<String, Any>, val barBar: Boolean, val bazBaz: Int)
30+
private data class Dst(val fooFoo: InnerDst, val bazBaz: Int)
31+
32+
companion object {
33+
private val src = Src(InnerSrc(1, 2, "three", "poiPoi" to 5), true, 4)
34+
private val snakeSrc = SnakeSrc(InnerSnakeSrc(1, 2, "three", "poi_poi" to 5), true, 4)
35+
private val mapSrc = MapSrc(mapOf("hogeHoge" to 1, "piyoPiyo" to "three", "mogeMoge" to ("poiPoi" to 5)), true, 4)
36+
private val expected = Dst(InnerDst(1, "three", InnerInnerDst(5)), 4)
37+
}
38+
39+
@Nested
40+
@DisplayName("KMapper")
41+
inner class KMapperTest {
42+
@Test
43+
@DisplayName("シンプルなマッピング")
44+
fun test() {
45+
val actual = KMapper(::Dst).map(src)
46+
assertEquals(expected, actual)
47+
}
48+
49+
@Test
50+
@DisplayName("スネークケースsrc -> キャメルケースdst")
51+
fun snakeToCamel() {
52+
val actual = KMapper(::Dst) {
53+
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it)
54+
}.map(snakeSrc)
55+
assertEquals(expected, actual)
56+
}
57+
58+
@Test
59+
@DisplayName("内部フィールドがMapの場合")
60+
fun includesMap() {
61+
val actual = KMapper(::Dst).map(mapSrc)
62+
assertEquals(expected, actual)
63+
}
64+
}
65+
66+
@Nested
67+
@DisplayName("PlainKMapper")
68+
inner class PlainKMapperTest {
69+
@Test
70+
@DisplayName("シンプルなマッピング")
71+
fun test() {
72+
val actual = PlainKMapper(::Dst).map(src)
73+
assertEquals(expected, actual)
74+
}
75+
76+
@Test
77+
@DisplayName("スネークケースsrc -> キャメルケースdst")
78+
fun snakeToCamel() {
79+
val actual = PlainKMapper(::Dst) {
80+
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it)
81+
}.map(snakeSrc)
82+
assertEquals(expected, actual)
83+
}
84+
85+
@Test
86+
@DisplayName("内部フィールドがMapの場合")
87+
fun includesMap() {
88+
val actual = PlainKMapper(::Dst).map(mapSrc)
89+
assertEquals(expected, actual)
90+
}
91+
}
92+
93+
@Nested
94+
@DisplayName("BoundKMapper")
95+
inner class BoundKMapperTest {
96+
@Test
97+
@DisplayName("シンプルなマッピング")
98+
fun test() {
99+
val actual = BoundKMapper(::Dst, Src::class).map(src)
100+
assertEquals(expected, actual)
101+
}
102+
103+
@Test
104+
@DisplayName("スネークケースsrc -> キャメルケースdst")
105+
fun snakeToCamel() {
106+
val actual = BoundKMapper(::Dst, SnakeSrc::class) {
107+
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it)
108+
}.map(snakeSrc)
109+
assertEquals(expected, actual)
110+
}
111+
112+
@Test
113+
@DisplayName("内部フィールドがMapの場合")
114+
fun includesMap() {
115+
val actual = BoundKMapper(::Dst, MapSrc::class).map(mapSrc)
116+
assertEquals(expected, actual)
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)