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

Commit bfdd0ea

Browse files
authored
Merge pull request #19 from k163377/feature
Add BoundKMapper.
2 parents 690bba4 + 53b8324 commit bfdd0ea

13 files changed

+339
-52
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.19"
9+
version = "0.20"
1010

1111
java {
1212
sourceCompatibility = JavaVersion.VERSION_1_8
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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.IllegalArgumentException
11+
import kotlin.reflect.KClass
12+
import kotlin.reflect.KFunction
13+
import kotlin.reflect.KParameter
14+
import kotlin.reflect.KProperty1
15+
import kotlin.reflect.KVisibility
16+
import kotlin.reflect.full.findAnnotation
17+
import kotlin.reflect.full.memberProperties
18+
import kotlin.reflect.jvm.jvmName
19+
20+
class BoundKMapper<S : Any, D : Any> private constructor(
21+
private val function: KFunctionForCall<D>,
22+
src: KClass<S>,
23+
parameterNameConverter: (String) -> String = { it }
24+
) {
25+
constructor(function: KFunction<D>, src: KClass<S>, parameterNameConverter: (String) -> String = { it }) : this(
26+
KFunctionForCall(function), src, parameterNameConverter
27+
)
28+
29+
constructor(clazz: KClass<D>, src: KClass<S>, parameterNameConverter: (String) -> String = { it }) : this(
30+
clazz.toKConstructor(), src, parameterNameConverter
31+
)
32+
33+
private val parameters: List<BoundParameterForMap<S>>
34+
35+
init {
36+
val srcPropertiesMap: Map<String, KProperty1<S, *>> =
37+
src.memberProperties
38+
.filter {
39+
// アクセス可能かつignoreされてないもののみ抽出
40+
!(it.visibility != KVisibility.PUBLIC) &&
41+
it.getter.annotations.none { annotation -> annotation is KGetterIgnore }
42+
}.associateBy { it.getter.findAnnotation<KGetterAlias>()?.value ?: it.name }
43+
44+
parameters = function.parameters
45+
.filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() }
46+
.mapNotNull {
47+
val temp = srcPropertiesMap[parameterNameConverter(it.getAliasOrName()!!)]?.let { property ->
48+
BoundParameterForMap(it, property)
49+
}
50+
51+
// 必須引数に対応するプロパティがsrcに定義されていない場合エラー
52+
if (temp == null && !it.isOptional) {
53+
throw IllegalArgumentException("Property ${it.name!!} is not declared in ${src.jvmName}.")
54+
}
55+
56+
temp
57+
}
58+
}
59+
60+
fun map(src: S): D {
61+
val bucket: ArgumentBucket = function.getArgumentBucket()
62+
63+
parameters.forEach {
64+
bucket.putIfAbsent(it.param, it.map(src))
65+
}
66+
67+
return function.call(bucket)
68+
}
69+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.mapk.kmapper
2+
3+
import com.mapk.core.EnumMapper
4+
import java.lang.IllegalArgumentException
5+
import kotlin.reflect.KClass
6+
import kotlin.reflect.KParameter
7+
import kotlin.reflect.KProperty1
8+
import kotlin.reflect.full.isSubclassOf
9+
import kotlin.reflect.jvm.javaGetter
10+
11+
@Suppress("UNCHECKED_CAST")
12+
internal class BoundParameterForMap<S : Any>(val param: KParameter, property: KProperty1<S, *>) {
13+
val map: (S) -> Any?
14+
15+
init {
16+
// ゲッターが無いならエラー
17+
val propertyGetter = property.javaGetter
18+
?: throw IllegalArgumentException("${property.name} does not have getter.")
19+
propertyGetter.isAccessible = true
20+
21+
val paramClazz = param.type.classifier as KClass<*>
22+
val propertyClazz = property.returnType.classifier as KClass<*>
23+
24+
val converter = (convertersFromConstructors(paramClazz) +
25+
convertersFromStaticMethods(paramClazz) +
26+
convertersFromCompanionObject(paramClazz))
27+
.filter { (key, _) -> propertyClazz.isSubclassOf(key) }
28+
.let {
29+
if (1 < it.size) throw IllegalArgumentException("${param.name} has multiple converter. $it")
30+
31+
it.singleOrNull()?.second
32+
}
33+
34+
map = when {
35+
converter != null -> { { converter.call(propertyGetter.invoke(it)) } }
36+
paramClazz.isSubclassOf(propertyClazz) -> { { propertyGetter.invoke(it) } }
37+
paramClazz.java.isEnum && propertyClazz == String::class -> { {
38+
EnumMapper.getEnum(paramClazz.java, propertyGetter.invoke(it) as String)
39+
} }
40+
paramClazz == String::class -> { { propertyGetter.invoke(it).toString() } }
41+
else -> throw IllegalArgumentException("Can not convert $propertyClazz to $paramClazz")
42+
}
43+
}
44+
}
Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
package com.mapk.kmapper
22

3-
import com.mapk.annotations.KConverter
4-
import com.mapk.core.KFunctionWithInstance
53
import kotlin.reflect.KClass
64
import kotlin.reflect.KFunction
75
import kotlin.reflect.KParameter
8-
import kotlin.reflect.full.companionObjectInstance
9-
import kotlin.reflect.full.functions
106
import kotlin.reflect.full.isSubclassOf
11-
import kotlin.reflect.full.staticFunctions
12-
import kotlin.reflect.jvm.isAccessible
137

148
internal class ParameterForMap<T : Any> private constructor(val param: KParameter, val clazz: KClass<T>) {
159
val javaClazz: Class<T> by lazy {
@@ -30,39 +24,3 @@ internal class ParameterForMap<T : Any> private constructor(val param: KParamete
3024
}
3125
}
3226
}
33-
34-
private fun <T> Collection<KFunction<T>>.getConverterMapFromFunctions(): Set<Pair<KClass<*>, KFunction<T>>> {
35-
return filter { it.annotations.any { annotation -> annotation is KConverter } }
36-
.map { func ->
37-
func.isAccessible = true
38-
39-
(func.parameters.single().type.classifier as KClass<*>) to func
40-
}.toSet()
41-
}
42-
43-
private fun <T : Any> convertersFromConstructors(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
44-
return clazz.constructors.getConverterMapFromFunctions()
45-
}
46-
47-
@Suppress("UNCHECKED_CAST")
48-
private fun <T : Any> convertersFromStaticMethods(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
49-
val staticFunctions: Collection<KFunction<T>> = clazz.staticFunctions as Collection<KFunction<T>>
50-
51-
return staticFunctions.getConverterMapFromFunctions()
52-
}
53-
54-
@Suppress("UNCHECKED_CAST")
55-
private fun <T : Any> convertersFromCompanionObject(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
56-
return clazz.companionObjectInstance?.let { companionObject ->
57-
companionObject::class.functions
58-
.filter { it.annotations.any { annotation -> annotation is KConverter } }
59-
.map { function ->
60-
val func: KFunction<T> = KFunctionWithInstance(
61-
function,
62-
companionObject
63-
) as KFunction<T>
64-
65-
(func.parameters.single().type.classifier as KClass<*>) to func
66-
}.toSet()
67-
} ?: emptySet()
68-
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.mapk.kmapper
2+
3+
import com.mapk.annotations.KConverter
4+
import com.mapk.core.KFunctionWithInstance
5+
import kotlin.reflect.KClass
6+
import kotlin.reflect.KFunction
7+
import kotlin.reflect.full.companionObjectInstance
8+
import kotlin.reflect.full.functions
9+
import kotlin.reflect.full.staticFunctions
10+
import kotlin.reflect.jvm.isAccessible
11+
12+
internal fun <T> Collection<KFunction<T>>.getConverterMapFromFunctions(): Set<Pair<KClass<*>, KFunction<T>>> {
13+
return filter { it.annotations.any { annotation -> annotation is KConverter } }
14+
.map { func ->
15+
func.isAccessible = true
16+
17+
(func.parameters.single().type.classifier as KClass<*>) to func
18+
}.toSet()
19+
}
20+
21+
internal fun <T : Any> convertersFromConstructors(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
22+
return clazz.constructors.getConverterMapFromFunctions()
23+
}
24+
25+
@Suppress("UNCHECKED_CAST")
26+
internal fun <T : Any> convertersFromStaticMethods(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
27+
val staticFunctions: Collection<KFunction<T>> = clazz.staticFunctions as Collection<KFunction<T>>
28+
29+
return staticFunctions.getConverterMapFromFunctions()
30+
}
31+
32+
@Suppress("UNCHECKED_CAST")
33+
internal fun <T : Any> convertersFromCompanionObject(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
34+
return clazz.companionObjectInstance?.let { companionObject ->
35+
companionObject::class.functions
36+
.filter { it.annotations.any { annotation -> annotation is KConverter } }
37+
.map { function ->
38+
val func: KFunction<T> = KFunctionWithInstance(
39+
function,
40+
companionObject
41+
) as KFunction<T>
42+
43+
(func.parameters.single().type.classifier as KClass<*>) to func
44+
}.toSet()
45+
} ?: emptySet()
46+
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ class CompanionConverter private constructor(val arg: String) {
2525

2626
private data class StaticMethodConverterDst(val argument: StaticMethodConverter)
2727

28+
private data class BoundConstructorConverterSrc(val argument: Int)
29+
private data class BoundCompanionConverterSrc(val argument: String)
30+
private data class BoundStaticMethodConverterSrc(val argument: String)
31+
2832
@DisplayName("コンバータ有りでのマッピングテスト")
2933
class ConverterKMapperTest {
3034
@Nested
@@ -57,4 +61,35 @@ class ConverterKMapperTest {
5761
assertTrue(intArrayOf(1, 2, 3) contentEquals result.argument.arg)
5862
}
5963
}
64+
65+
@Nested
66+
@DisplayName("BoundKMapper")
67+
inner class BoundKMapperTest {
68+
@Test
69+
@DisplayName("コンストラクターでのコンバートテスト")
70+
fun constructorConverterTest() {
71+
val mapper = BoundKMapper(::ConstructorConverterDst, BoundConstructorConverterSrc::class)
72+
val result = mapper.map(BoundConstructorConverterSrc(1))
73+
74+
assertEquals(ConstructorConverter(1), result.argument)
75+
}
76+
77+
@Test
78+
@DisplayName("コンパニオンオブジェクトに定義したコンバータでのコンバートテスト")
79+
fun companionConverterTest() {
80+
val mapper = BoundKMapper(::CompanionConverterDst, BoundCompanionConverterSrc::class)
81+
val result = mapper.map(BoundCompanionConverterSrc("arg"))
82+
83+
assertEquals("arg", result.argument.arg)
84+
}
85+
86+
@Test
87+
@DisplayName("スタティックメソッドに定義したコンバータでのコンバートテスト")
88+
fun staticMethodConverterTest() {
89+
val mapper = BoundKMapper(::StaticMethodConverterDst, BoundStaticMethodConverterSrc::class)
90+
val result = mapper.map(BoundStaticMethodConverterSrc("1,2,3"))
91+
92+
assertTrue(intArrayOf(1, 2, 3) contentEquals result.argument.arg)
93+
}
94+
}
6095
}

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,25 @@ class DefaultArgumentTest {
1111
data class Dst(val fooArgument: Int, @param:KUseDefaultArgument val barArgument: String = "default")
1212
data class Src(val fooArgument: Int, val barArgument: String)
1313

14+
private val src = Src(1, "src")
15+
1416
@Nested
1517
@DisplayName("KMapper")
1618
inner class KMapperTest {
1719
@Test
1820
fun test() {
19-
val src = Src(1, "src")
20-
2121
val result = KMapper(::Dst).map(src)
2222
assertEquals(Dst(1, "default"), result)
2323
}
2424
}
25+
26+
@Nested
27+
@DisplayName("BoundKMapper")
28+
inner class BoundKMapperTest {
29+
@Test
30+
fun test() {
31+
val result = BoundKMapper(::Dst, Src::class).map(src)
32+
assertEquals(Dst(1, "default"), result)
33+
}
34+
}
2535
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,20 @@ class EnumMappingTest {
2929
assertEquals(language, result.language)
3030
}
3131
}
32+
33+
data class BoundSrc(val language: String)
34+
35+
@Nested
36+
@DisplayName("BoundKMapper")
37+
inner class BoundKMapperTest {
38+
private val mapper = BoundKMapper(::EnumMappingDst, BoundSrc::class)
39+
40+
@ParameterizedTest(name = "Non-Null要求")
41+
@EnumSource(value = JvmLanguage::class)
42+
fun test(language: JvmLanguage) {
43+
val result = mapper.map(BoundSrc(language.name))
44+
45+
assertEquals(language, result.language)
46+
}
47+
}
3248
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,21 @@ class KGetterIgnoreTest {
3131
assertEquals(Dst(1, "2-1", 32, "4"), dst1)
3232
}
3333
}
34+
35+
data class BoundSrc(val arg1: Int, val arg2: Short, @get:KGetterIgnore val arg3: String)
36+
data class BoundDst(val arg1: Int, val arg2: Short, val arg3: String = "default")
37+
38+
@Nested
39+
@DisplayName("BoundKMapper")
40+
inner class BoundKMapperTest {
41+
@Test
42+
@DisplayName("フィールドを無視するテスト")
43+
fun test() {
44+
val mapper = BoundKMapper(::BoundDst, BoundSrc::class)
45+
46+
val result = mapper.map(BoundSrc(1, 2, "arg3"))
47+
48+
assertEquals(BoundDst(1, 2, "default"), result)
49+
}
50+
}
3451
}

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import org.junit.jupiter.api.DisplayName
66
import org.junit.jupiter.api.Nested
77
import org.junit.jupiter.api.Test
88

9-
private class CamelCaseDst(val camelCase: String)
9+
private data class CamelCaseDst(val camelCase: String)
10+
private data class BoundSrc(val camel_case: String)
1011

1112
@DisplayName("パラメータ名変換のテスト")
1213
class ParameterNameConverterTest {
@@ -20,14 +21,27 @@ class ParameterNameConverterTest {
2021
val src = mapOf("camel_case" to expected)
2122

2223
val mapper = KMapper(CamelCaseDst::class) {
23-
CaseFormat.LOWER_CAMEL.to(
24-
CaseFormat.LOWER_UNDERSCORE,
25-
it
26-
)
24+
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it)
2725
}
2826
val result = mapper.map(src)
2927

3028
assertEquals(expected, result.camelCase)
3129
}
3230
}
31+
32+
@Nested
33+
@DisplayName("BoundKMapper")
34+
inner class BoundKMapperTest {
35+
@Test
36+
@DisplayName("スネークケースsrc -> キャメルケースdst")
37+
fun test() {
38+
39+
val mapper = BoundKMapper(::CamelCaseDst, BoundSrc::class) {
40+
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it)
41+
}
42+
val result = mapper.map(BoundSrc("snakeCase"))
43+
44+
assertEquals(CamelCaseDst("snakeCase"), result)
45+
}
46+
}
3347
}

0 commit comments

Comments
 (0)