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

Commit 66006aa

Browse files
authored
Merge pull request #23 from k163377/convert_cache
Add convert cache.
2 parents b052918 + 5663ade commit 66006aa

14 files changed

+397
-17
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.20"
9+
version = "0.21"
1010

1111
java {
1212
sourceCompatibility = JavaVersion.VERSION_1_8

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ internal class BoundParameterForMap<S : Any>(val param: KParameter, property: KP
2121
val paramClazz = param.type.classifier as KClass<*>
2222
val propertyClazz = property.returnType.classifier as KClass<*>
2323

24-
val converter = (convertersFromConstructors(paramClazz) +
25-
convertersFromStaticMethods(paramClazz) +
26-
convertersFromCompanionObject(paramClazz))
24+
val converter = paramClazz.getConverters()
2725
.filter { (key, _) -> propertyClazz.isSubclassOf(key) }
2826
.let {
2927
if (1 < it.size) throw IllegalArgumentException("${param.name} has multiple converter. $it")
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.mapk.kmapper
2+
3+
import com.mapk.annotations.KGetterAlias
4+
import com.mapk.annotations.KGetterIgnore
5+
import com.mapk.core.ArgumentBucket
6+
import com.mapk.core.KFunctionForCall
7+
import com.mapk.core.getAliasOrName
8+
import com.mapk.core.isUseDefaultArgument
9+
import com.mapk.core.toKConstructor
10+
import java.lang.reflect.Method
11+
import kotlin.reflect.KClass
12+
import kotlin.reflect.KFunction
13+
import kotlin.reflect.KParameter
14+
import kotlin.reflect.KVisibility
15+
import kotlin.reflect.full.memberProperties
16+
import kotlin.reflect.jvm.javaGetter
17+
18+
class KMapper<T : Any> private constructor(
19+
private val function: KFunctionForCall<T>,
20+
parameterNameConverter: (String) -> String
21+
) {
22+
constructor(function: KFunction<T>, parameterNameConverter: (String) -> String = { it }) : this(
23+
KFunctionForCall(function), parameterNameConverter
24+
)
25+
26+
constructor(clazz: KClass<T>, parameterNameConverter: (String) -> String = { it }) : this(
27+
clazz.toKConstructor(), parameterNameConverter
28+
)
29+
30+
private val parameterMap: Map<String, ParameterForMap<*>> = function.parameters
31+
.filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() }
32+
.associate { (parameterNameConverter(it.getAliasOrName()!!)) to ParameterForMap.newInstance(it) }
33+
34+
private fun bindArguments(argumentBucket: ArgumentBucket, src: Any) {
35+
src::class.memberProperties.forEach outer@{ property ->
36+
// propertyが公開されていない場合は処理を行わない
37+
if (property.visibility != KVisibility.PUBLIC) return@outer
38+
39+
// ゲッターが取れない場合は処理を行わない
40+
val javaGetter: Method = property.javaGetter ?: return@outer
41+
42+
var alias: String? = null
43+
// NOTE: IgnoreとAliasが同時に指定されるようなパターンを考慮してaliasが取れてもbreakしていない
44+
javaGetter.annotations.forEach {
45+
if (it is KGetterIgnore) return@outer // ignoreされている場合は処理を行わない
46+
if (it is KGetterAlias) alias = it.value
47+
}
48+
49+
parameterMap[alias ?: property.name]?.let {
50+
// javaGetterを呼び出す方が高速
51+
javaGetter.isAccessible = true
52+
argumentBucket.putIfAbsent(it.param, javaGetter.invoke(src)?.let { value -> it.mapObject(value) })
53+
// 終了判定
54+
if (argumentBucket.isInitialized) return
55+
}
56+
}
57+
}
58+
59+
private fun bindArguments(argumentBucket: ArgumentBucket, src: Map<*, *>) {
60+
src.forEach { (key, value) ->
61+
parameterMap[key]?.let { param ->
62+
// 取得した内容がnullでなければ適切にmapする
63+
argumentBucket.putIfAbsent(param.param, value?.let { param.mapObject(value) })
64+
// 終了判定
65+
if (argumentBucket.isInitialized) return
66+
}
67+
}
68+
}
69+
70+
private fun bindArguments(argumentBucket: ArgumentBucket, srcPair: Pair<*, *>) {
71+
parameterMap[srcPair.first.toString()]?.let {
72+
argumentBucket.putIfAbsent(it.param, srcPair.second?.let { value -> it.mapObject(value) })
73+
}
74+
}
75+
76+
fun map(srcMap: Map<String, Any?>): T {
77+
val bucket: ArgumentBucket = function.getArgumentBucket()
78+
bindArguments(bucket, srcMap)
79+
80+
return function.call(bucket)
81+
}
82+
83+
fun map(srcPair: Pair<String, Any?>): T {
84+
val bucket: ArgumentBucket = function.getArgumentBucket()
85+
bindArguments(bucket, srcPair)
86+
87+
return function.call(bucket)
88+
}
89+
90+
fun map(src: Any): T {
91+
val bucket: ArgumentBucket = function.getArgumentBucket()
92+
bindArguments(bucket, src)
93+
94+
return function.call(bucket)
95+
}
96+
97+
fun map(vararg args: Any): T {
98+
val bucket: ArgumentBucket = function.getArgumentBucket()
99+
100+
listOf(*args).forEach { arg ->
101+
when (arg) {
102+
is Map<*, *> -> bindArguments(bucket, arg)
103+
is Pair<*, *> -> bindArguments(bucket, arg)
104+
else -> bindArguments(bucket, arg)
105+
}
106+
}
107+
108+
return function.call(bucket)
109+
}
110+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.mapk.kmapper
2+
3+
import com.mapk.core.EnumMapper
4+
import kotlin.reflect.KClass
5+
import kotlin.reflect.KFunction
6+
import kotlin.reflect.KParameter
7+
import kotlin.reflect.full.isSuperclassOf
8+
9+
internal class ParameterForMap<T : Any> private constructor(val param: KParameter, private val clazz: KClass<T>) {
10+
private val javaClazz: Class<T> by lazy {
11+
clazz.java
12+
}
13+
// リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい
14+
private val converters: Set<Pair<KClass<*>, KFunction<T>>> = clazz.getConverters()
15+
16+
private val convertCache: MutableMap<KClass<*>, (Any) -> Any?> = HashMap()
17+
18+
fun <U : Any> mapObject(value: U): Any? {
19+
val valueClazz: KClass<*> = value::class
20+
21+
// 取得方法のキャッシュが有ればそれを用いる
22+
convertCache[valueClazz]?.let { return it(value) }
23+
24+
// パラメータに対してvalueが代入可能(同じもしくは親クラス)であればそのまま用いる
25+
if (clazz.isSuperclassOf(valueClazz)) {
26+
convertCache[valueClazz] = { value }
27+
return value
28+
}
29+
30+
val converter: KFunction<*>? = converters.getConverter(valueClazz)
31+
32+
val lambda: (Any) -> Any? = when {
33+
// converterに一致する組み合わせが有れば設定されていればそれを使う
34+
converter != null -> { { converter.call(it) } }
35+
// 要求された値がenumかつ元が文字列ならenum mapperでマップ
36+
javaClazz.isEnum && value is String -> { { EnumMapper.getEnum(javaClazz, it as String) } }
37+
// 要求されているパラメータがStringならtoStringする
38+
clazz == String::class -> { { it.toString() } }
39+
else -> throw IllegalArgumentException("Can not convert $valueClazz to $clazz")
40+
}
41+
convertCache[valueClazz] = lambda
42+
return lambda(value)
43+
}
44+
45+
companion object {
46+
fun newInstance(param: KParameter): ParameterForMap<*> {
47+
return ParameterForMap(param, param.type.classifier as KClass<*>)
48+
}
49+
}
50+
}

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import kotlin.reflect.KClass
66
import kotlin.reflect.KFunction
77
import kotlin.reflect.full.companionObjectInstance
88
import kotlin.reflect.full.functions
9+
import kotlin.reflect.full.isSubclassOf
910
import kotlin.reflect.full.staticFunctions
1011
import kotlin.reflect.jvm.isAccessible
1112

12-
internal fun <T> Collection<KFunction<T>>.getConverterMapFromFunctions(): Set<Pair<KClass<*>, KFunction<T>>> {
13+
internal fun <T : Any> KClass<T>.getConverters(): Set<Pair<KClass<*>, KFunction<T>>> =
14+
convertersFromConstructors(this) + convertersFromStaticMethods(this) + convertersFromCompanionObject(this)
15+
16+
private fun <T> Collection<KFunction<T>>.getConverterMapFromFunctions(): Set<Pair<KClass<*>, KFunction<T>>> {
1317
return filter { it.annotations.any { annotation -> annotation is KConverter } }
1418
.map { func ->
1519
func.isAccessible = true
@@ -18,19 +22,19 @@ internal fun <T> Collection<KFunction<T>>.getConverterMapFromFunctions(): Set<Pa
1822
}.toSet()
1923
}
2024

21-
internal fun <T : Any> convertersFromConstructors(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
25+
private fun <T : Any> convertersFromConstructors(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
2226
return clazz.constructors.getConverterMapFromFunctions()
2327
}
2428

2529
@Suppress("UNCHECKED_CAST")
26-
internal fun <T : Any> convertersFromStaticMethods(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
30+
private fun <T : Any> convertersFromStaticMethods(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
2731
val staticFunctions: Collection<KFunction<T>> = clazz.staticFunctions as Collection<KFunction<T>>
2832

2933
return staticFunctions.getConverterMapFromFunctions()
3034
}
3135

3236
@Suppress("UNCHECKED_CAST")
33-
internal fun <T : Any> convertersFromCompanionObject(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
37+
private fun <T : Any> convertersFromCompanionObject(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
3438
return clazz.companionObjectInstance?.let { companionObject ->
3539
companionObject::class.functions
3640
.filter { it.annotations.any { annotation -> annotation is KConverter } }
@@ -44,3 +48,7 @@ internal fun <T : Any> convertersFromCompanionObject(clazz: KClass<T>): Set<Pair
4448
}.toSet()
4549
} ?: emptySet()
4650
}
51+
52+
// 引数の型がconverterに対して入力可能ならconverterを返す
53+
internal fun <T : Any> Set<Pair<KClass<*>, KFunction<T>>>.getConverter(input: KClass<out T>): KFunction<T>? =
54+
this.find { (key, _) -> input.isSubclassOf(key) }?.second

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,22 @@ import com.mapk.core.EnumMapper
44
import kotlin.reflect.KClass
55
import kotlin.reflect.KFunction
66
import kotlin.reflect.KParameter
7-
import kotlin.reflect.full.isSubclassOf
87
import kotlin.reflect.full.isSuperclassOf
98

109
internal class PlainParameterForMap<T : Any> private constructor(val param: KParameter, private val clazz: KClass<T>) {
1110
private val javaClazz: Class<T> by lazy {
1211
clazz.java
1312
}
1413
// リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい
15-
private val converters: Set<Pair<KClass<*>, KFunction<T>>> by lazy {
16-
convertersFromConstructors(clazz) + convertersFromStaticMethods(clazz) + convertersFromCompanionObject(clazz)
17-
}
14+
private val converters: Set<Pair<KClass<*>, KFunction<T>>> = clazz.getConverters()
1815

1916
fun <U : Any> mapObject(value: U): Any? {
2017
val valueClazz: KClass<*> = value::class
2118

2219
// パラメータに対してvalueが代入可能(同じもしくは親クラス)であればそのまま用いる
2320
if (clazz.isSuperclassOf(valueClazz)) return value
2421

25-
val converter: KFunction<*>? = getConverter(valueClazz)
22+
val converter: KFunction<*>? = converters.getConverter(valueClazz)
2623

2724
return when {
2825
// converterに一致する組み合わせが有れば設定されていればそれを使う
@@ -35,10 +32,6 @@ internal class PlainParameterForMap<T : Any> private constructor(val param: KPar
3532
}
3633
}
3734

38-
// 引数の型がconverterに対して入力可能ならconverterを返す
39-
private fun <R : Any> getConverter(input: KClass<out R>): KFunction<T>? =
40-
converters.find { (key, _) -> input.isSubclassOf(key) }?.second
41-
4235
companion object {
4336
fun newInstance(param: KParameter): PlainParameterForMap<*> {
4437
return PlainParameterForMap(param, param.type.classifier as KClass<*>)

src/test/kotlin/com/mapk/kmapper/ConverterKMapperTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,37 @@ private data class BoundStaticMethodConverterSrc(val argument: String)
3131

3232
@DisplayName("コンバータ有りでのマッピングテスト")
3333
class ConverterKMapperTest {
34+
@Nested
35+
@DisplayName("KMapper")
36+
inner class KMapperTest {
37+
@Test
38+
@DisplayName("コンストラクターでのコンバートテスト")
39+
fun constructorConverterTest() {
40+
val mapper = KMapper(ConstructorConverterDst::class)
41+
val result = mapper.map(mapOf("argument" to 1))
42+
43+
assertEquals(ConstructorConverter(1), result.argument)
44+
}
45+
46+
@Test
47+
@DisplayName("コンパニオンオブジェクトに定義したコンバータでのコンバートテスト")
48+
fun companionConverterTest() {
49+
val mapper = KMapper(CompanionConverterDst::class)
50+
val result = mapper.map(mapOf("argument" to "arg"))
51+
52+
assertEquals("arg", result.argument.arg)
53+
}
54+
55+
@Test
56+
@DisplayName("スタティックメソッドに定義したコンバータでのコンバートテスト")
57+
fun staticMethodConverterTest() {
58+
val mapper = KMapper(StaticMethodConverterDst::class)
59+
val result = mapper.map(mapOf("argument" to "1,2,3"))
60+
61+
assertTrue(intArrayOf(1, 2, 3) contentEquals result.argument.arg)
62+
}
63+
}
64+
3465
@Nested
3566
@DisplayName("PlainKMapper")
3667
inner class PlainKMapperTest {

src/test/kotlin/com/mapk/kmapper/DefaultArgumentTest.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ class DefaultArgumentTest {
1313

1414
private val src = Src(1, "src")
1515

16+
@Nested
17+
@DisplayName("KMapper")
18+
inner class KMapperTest {
19+
@Test
20+
fun test() {
21+
val result = KMapper(::Dst).map(src)
22+
assertEquals(Dst(1, "default"), result)
23+
}
24+
}
25+
1626
@Nested
1727
@DisplayName("PlainKMapper")
1828
inner class PlainKMapperTest {

src/test/kotlin/com/mapk/kmapper/EnumMappingTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ private class EnumMappingDst(val language: JvmLanguage?)
1616

1717
@DisplayName("文字列 -> Enumのマッピングテスト")
1818
class EnumMappingTest {
19+
@Nested
20+
@DisplayName("KMapper")
21+
inner class KMapperTest {
22+
private val mapper = KMapper(EnumMappingDst::class)
23+
24+
@ParameterizedTest(name = "Non-Null要求")
25+
@EnumSource(value = JvmLanguage::class)
26+
fun test(language: JvmLanguage) {
27+
val result = mapper.map("language" to language.name)
28+
29+
assertEquals(language, result.language)
30+
}
31+
}
32+
1933
@Nested
2034
@DisplayName("PlainKMapper")
2135
inner class PlainKMapperTest {

src/test/kotlin/com/mapk/kmapper/KGetterIgnoreTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,25 @@ class KGetterIgnoreTest {
1313

1414
data class Dst(val arg1: Int, val arg2: String, val arg3: Int, val arg4: String)
1515

16+
@Nested
17+
@DisplayName("KMapper")
18+
inner class KMapperTest {
19+
@Test
20+
@DisplayName("フィールドを無視するテスト")
21+
fun test() {
22+
val src1 = Src1(1, "2-1", 31)
23+
val src2 = Src2("2-2", 32, "4")
24+
25+
val mapper = KMapper(::Dst)
26+
27+
val dst1 = mapper.map(src1, src2)
28+
val dst2 = mapper.map(src2, src1)
29+
30+
assertTrue(dst1 == dst2)
31+
assertEquals(Dst(1, "2-1", 32, "4"), dst1)
32+
}
33+
}
34+
1635
@Nested
1736
@DisplayName("PlainKMapper")
1837
inner class PlainKMapperTest {

0 commit comments

Comments
 (0)