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

Commit c9f2285

Browse files
authored
Merge pull request #28 from k163377/feature
Add parameter conversion support.
2 parents 4518692 + 5ee465f commit c9f2285

File tree

7 files changed

+175
-7
lines changed

7 files changed

+175
-7
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.24"
9+
version = "0.25"
1010

1111
java {
1212
sourceCompatibility = JavaVersion.VERSION_1_8
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.mapk.conversion
2+
3+
import kotlin.reflect.KClass
4+
5+
@Target(AnnotationTarget.ANNOTATION_CLASS)
6+
@Retention(AnnotationRetention.RUNTIME)
7+
@MustBeDocumented
8+
annotation class KConvertBy(val converters: Array<KClass<out AbstractKConverter<*, *, *>>>)
9+
10+
abstract class AbstractKConverter<A : Annotation, S : Any, D : Any>(protected val annotation: A) {
11+
abstract val srcClass: KClass<S>
12+
abstract fun convert(source: S?): D?
13+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ internal sealed class BoundParameterForMap<S> {
7979
val propertyClazz = property.returnType.classifier as KClass<*>
8080

8181
// コンバータが取れた場合
82-
paramClazz.getConverters()
82+
(param.getConverters() + paramClazz.getConverters())
8383
.filter { (key, _) -> propertyClazz.isSubclassOf(key) }
8484
.let {
8585
if (1 < it.size) throw IllegalArgumentException("${param.name} has multiple converter. $it")

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ internal class ParameterForMap<T : Any> private constructor(
1717
clazz.java
1818
}
1919
// リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい
20-
private val converters: Set<Pair<KClass<*>, KFunction<T>>> = clazz.getConverters()
20+
@Suppress("UNCHECKED_CAST")
21+
private val converters: Set<Pair<KClass<*>, KFunction<T>>> =
22+
(param.getConverters() as Set<Pair<KClass<*>, KFunction<T>>>) + clazz.getConverters()
2123

2224
private val convertCache: ConcurrentMap<KClass<*>, ParameterProcessor> = ConcurrentHashMap()
2325

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
package com.mapk.kmapper
22

33
import com.mapk.annotations.KConverter
4+
import com.mapk.conversion.KConvertBy
45
import com.mapk.core.KFunctionWithInstance
56
import kotlin.reflect.KClass
67
import kotlin.reflect.KFunction
8+
import kotlin.reflect.KParameter
79
import kotlin.reflect.full.companionObjectInstance
10+
import kotlin.reflect.full.findAnnotation
811
import kotlin.reflect.full.functions
912
import kotlin.reflect.full.isSubclassOf
13+
import kotlin.reflect.full.primaryConstructor
1014
import kotlin.reflect.full.staticFunctions
1115
import kotlin.reflect.jvm.isAccessible
1216

1317
internal fun <T : Any> KClass<T>.getConverters(): Set<Pair<KClass<*>, KFunction<T>>> =
1418
convertersFromConstructors(this) + convertersFromStaticMethods(this) + convertersFromCompanionObject(this)
1519

16-
private fun <T> Collection<KFunction<T>>.getConverterMapFromFunctions(): Set<Pair<KClass<*>, KFunction<T>>> {
20+
private fun <T> Collection<KFunction<T>>.getConvertersFromFunctions(): Set<Pair<KClass<*>, KFunction<T>>> {
1721
return filter { it.annotations.any { annotation -> annotation is KConverter } }
1822
.map { func ->
1923
func.isAccessible = true
@@ -23,14 +27,14 @@ private fun <T> Collection<KFunction<T>>.getConverterMapFromFunctions(): Set<Pai
2327
}
2428

2529
private fun <T : Any> convertersFromConstructors(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
26-
return clazz.constructors.getConverterMapFromFunctions()
30+
return clazz.constructors.getConvertersFromFunctions()
2731
}
2832

2933
@Suppress("UNCHECKED_CAST")
3034
private fun <T : Any> convertersFromStaticMethods(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
3135
val staticFunctions: Collection<KFunction<T>> = clazz.staticFunctions as Collection<KFunction<T>>
3236

33-
return staticFunctions.getConverterMapFromFunctions()
37+
return staticFunctions.getConvertersFromFunctions()
3438
}
3539

3640
@Suppress("UNCHECKED_CAST")
@@ -49,6 +53,16 @@ private fun <T : Any> convertersFromCompanionObject(clazz: KClass<T>): Set<Pair<
4953
} ?: emptySet()
5054
}
5155

56+
@Suppress("UNCHECKED_CAST")
57+
internal fun KParameter.getConverters(): Set<Pair<KClass<*>, KFunction<*>>> {
58+
return annotations.mapNotNull { paramAnnotation ->
59+
paramAnnotation.annotationClass
60+
.findAnnotation<KConvertBy>()
61+
?.converters
62+
?.map { it.primaryConstructor!!.call(paramAnnotation) }
63+
}.flatten().map { (it.srcClass) to it::convert as KFunction<*> }.toSet()
64+
}
65+
5266
// 引数の型がconverterに対して入力可能ならconverterを返す
5367
internal fun <T : Any> Set<Pair<KClass<*>, KFunction<T>>>.getConverter(input: KClass<out T>): KFunction<T>? =
5468
this.find { (key, _) -> input.isSubclassOf(key) }?.second

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ internal class PlainParameterForMap<T : Any> private constructor(
1515
clazz.java
1616
}
1717
// リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい
18-
private val converters: Set<Pair<KClass<*>, KFunction<T>>> = clazz.getConverters()
18+
@Suppress("UNCHECKED_CAST")
19+
private val converters: Set<Pair<KClass<*>, KFunction<T>>> =
20+
(param.getConverters() as Set<Pair<KClass<*>, KFunction<T>>>) + clazz.getConverters()
1921

2022
fun <U : Any> mapObject(value: U): Any? {
2123
val valueClazz: KClass<*> = value::class
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package com.mapk.kmapper
2+
3+
import com.mapk.conversion.AbstractKConverter
4+
import com.mapk.conversion.KConvertBy
5+
import java.lang.IllegalArgumentException
6+
import java.math.BigDecimal
7+
import java.math.BigInteger
8+
import kotlin.reflect.KClass
9+
import kotlin.reflect.jvm.jvmName
10+
import org.junit.jupiter.api.Assertions.assertEquals
11+
import org.junit.jupiter.api.DisplayName
12+
import org.junit.jupiter.api.Nested
13+
import org.junit.jupiter.params.ParameterizedTest
14+
import org.junit.jupiter.params.provider.EnumSource
15+
import org.junit.jupiter.params.provider.ValueSource
16+
17+
@DisplayName("KConvertアノテーションによる変換のテスト")
18+
class ConversionTest {
19+
@Target(AnnotationTarget.VALUE_PARAMETER)
20+
@Retention(AnnotationRetention.RUNTIME)
21+
@MustBeDocumented
22+
@KConvertBy([FromString::class, FromNumber::class])
23+
annotation class ToNumber(val destination: KClass<out Number>)
24+
25+
class FromString(annotation: ToNumber) : AbstractKConverter<ToNumber, String, Number>(annotation) {
26+
private val converter: (String) -> Number = when (annotation.destination) {
27+
Double::class -> String::toDouble
28+
Float::class -> String::toFloat
29+
Long::class -> String::toLong
30+
Int::class -> String::toInt
31+
Short::class -> String::toShort
32+
Byte::class -> String::toByte
33+
BigDecimal::class -> { { BigDecimal(it) } }
34+
BigInteger::class -> { { BigInteger(it) } }
35+
else -> throw IllegalArgumentException("${annotation.destination.jvmName} is not supported.")
36+
}
37+
38+
override val srcClass = String::class
39+
override fun convert(source: String?): Number? = source?.let(converter)
40+
}
41+
42+
class FromNumber(annotation: ToNumber) : AbstractKConverter<ToNumber, Number, Number>(annotation) {
43+
private val converter: (Number) -> Number = when (annotation.destination) {
44+
Double::class -> Number::toDouble
45+
Float::class -> Number::toFloat
46+
Long::class -> Number::toLong
47+
Int::class -> Number::toInt
48+
Short::class -> Number::toShort
49+
Byte::class -> Number::toByte
50+
BigDecimal::class -> { { BigDecimal.valueOf(it.toDouble()) } }
51+
BigInteger::class -> { { BigInteger.valueOf(it.toLong()) } }
52+
else -> throw IllegalArgumentException("${annotation.destination.jvmName} is not supported.")
53+
}
54+
55+
override val srcClass = Number::class
56+
override fun convert(source: Number?): Number? = source?.let(converter)
57+
}
58+
59+
data class Dst(@ToNumber(BigDecimal::class) val number: BigDecimal)
60+
data class NumberSrc(val number: Number)
61+
data class StringSrc(val number: String)
62+
63+
enum class NumberSource(val values: Array<Number>) {
64+
Doubles(arrayOf(1.0, -2.0, 3.5)),
65+
Floats(arrayOf(4.1f, -5.09f, 6.00001f)),
66+
Longs(arrayOf(7090, 800, 911)),
67+
Ints(arrayOf(0, 123, 234)),
68+
Shorts(arrayOf(365, 416, 511)),
69+
Bytes(arrayOf(6, 7, 8))
70+
}
71+
72+
@Nested
73+
@DisplayName("KMapper")
74+
inner class KMapperTest {
75+
@ParameterizedTest
76+
@EnumSource(NumberSource::class)
77+
@DisplayName("Numberソース")
78+
fun fromNumber(numbers: NumberSource) {
79+
numbers.values.forEach {
80+
val actual = KMapper(::Dst).map(NumberSrc(it))
81+
assertEquals(0, BigDecimal.valueOf(it.toDouble()).compareTo(actual.number))
82+
}
83+
}
84+
85+
@ParameterizedTest
86+
@ValueSource(strings = ["100", "2.0", "-500"])
87+
@DisplayName("Stringソース")
88+
fun fromString(str: String) {
89+
val actual = KMapper(::Dst).map(StringSrc(str))
90+
assertEquals(0, BigDecimal(str).compareTo(actual.number))
91+
}
92+
}
93+
94+
@Nested
95+
@DisplayName("PlainKMapper")
96+
inner class PlainKMapperTest {
97+
@ParameterizedTest
98+
@EnumSource(NumberSource::class)
99+
@DisplayName("Numberソース")
100+
fun fromNumber(numbers: NumberSource) {
101+
numbers.values.forEach {
102+
val actual = PlainKMapper(::Dst).map(NumberSrc(it))
103+
assertEquals(0, BigDecimal.valueOf(it.toDouble()).compareTo(actual.number))
104+
}
105+
}
106+
107+
@ParameterizedTest
108+
@ValueSource(strings = ["100", "2.0", "-500"])
109+
@DisplayName("Stringソース")
110+
fun fromString(str: String) {
111+
val actual = PlainKMapper(::Dst).map(StringSrc(str))
112+
assertEquals(0, BigDecimal(str).compareTo(actual.number))
113+
}
114+
}
115+
116+
@Nested
117+
@DisplayName("BoundKMapper")
118+
inner class BoundKMapperTest {
119+
@ParameterizedTest
120+
@EnumSource(NumberSource::class)
121+
@DisplayName("Numberソース")
122+
fun fromNumber(numbers: NumberSource) {
123+
numbers.values.forEach {
124+
val actual = BoundKMapper(::Dst, NumberSrc::class).map(NumberSrc(it))
125+
assertEquals(0, BigDecimal.valueOf(it.toDouble()).compareTo(actual.number))
126+
}
127+
}
128+
129+
@ParameterizedTest
130+
@ValueSource(strings = ["100", "2.0", "-500"])
131+
@DisplayName("Stringソース")
132+
fun fromString(str: String) {
133+
val actual = BoundKMapper(::Dst, StringSrc::class).map(StringSrc(str))
134+
assertEquals(0, BigDecimal(str).compareTo(actual.number))
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)