Skip to content
This repository was archived by the owner on Jan 20, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2ce4f5a
バージョンアップ
k163377 Apr 19, 2020
93ee440
再帰的マッピングのテスト基盤を追加
k163377 Apr 19, 2020
a65c397
再帰的マッピングのひな型を追加
k163377 Apr 19, 2020
cefbf62
パラメータ名変換をテストするためキャメルケース化
k163377 Apr 19, 2020
ba49c91
ネストしてパラメータ名変換を適用する場合のテストを追加
k163377 Apr 19, 2020
8f81979
再帰的にパラメータ名変換を適用するよう修正
k163377 Apr 19, 2020
7824bba
プロパティ名変換もパラメータに渡すよう修正
k163377 Apr 19, 2020
cbd79de
内部にMapを入れた場合のテストを追加
k163377 Apr 19, 2020
4431602
内部にMapやPairを差し込まれた時への対応を追加
k163377 Apr 19, 2020
9bbe4ab
KMapperに関する再帰的マッピングのテストを追加
k163377 Apr 19, 2020
4f2a569
ダミーは共通で使いたいため切り出し
k163377 Apr 19, 2020
f3072db
マッピング用のクラスを追加
k163377 Apr 19, 2020
e19d917
パラメータの振り分けを追加
k163377 Apr 19, 2020
4e08aca
引数の変更に対応
k163377 Apr 19, 2020
f854eba
pairを用いてネストしたマッピングをした際にも正常に動くことを確認するためのフィールドを追加
k163377 Apr 19, 2020
da277f8
PlainKMapperに関する再帰的マッピングのテストを追加
k163377 Apr 19, 2020
df695c9
振り分けを追加
k163377 Apr 19, 2020
0c580b7
引数を修正
k163377 Apr 19, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {
}

group = "com.mapk"
version = "0.23"
version = "0.24"

java {
sourceCompatibility = JavaVersion.VERSION_1_8
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class BoundKMapper<S : Any, D : Any> 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に定義されていない場合エラー
Expand Down
32 changes: 30 additions & 2 deletions src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ internal sealed class BoundParameterForMap<S> {
override fun map(src: S): Any? = converter.call(propertyGetter.invoke(src))
}

private class UseKMapper<S : Any>(
override val param: KParameter,
override val propertyGetter: Method,
private val kMapper: KMapper<*>
) : BoundParameterForMap<S>() {
// 1引数で呼び出すとMap/Pairが適切に処理されないため、2引数目にダミーを噛ませている
override fun map(src: S): Any? = kMapper.map(propertyGetter.invoke(src), PARAMETER_DUMMY)
}

private class UseBoundKMapper<S : Any, T : Any>(
override val param: KParameter,
override val propertyGetter: Method,
private val boundKMapper: BoundKMapper<T, *>
) : BoundParameterForMap<S>() {
override fun map(src: S): Any? = boundKMapper.map(propertyGetter.invoke(src) as T)
}

private class ToEnum<S : Any>(
override val param: KParameter,
override val propertyGetter: Method,
Expand All @@ -48,7 +65,11 @@ internal sealed class BoundParameterForMap<S> {
}

companion object {
fun <S : Any> newInstance(param: KParameter, property: KProperty1<S, *>): BoundParameterForMap<S> {
fun <S : Any> newInstance(
param: KParameter,
property: KProperty1<S, *>,
parameterNameConverter: (String) -> String
): BoundParameterForMap<S> {
// ゲッターが無いならエラー
val propertyGetter = property.javaGetter
?: throw IllegalArgumentException("${property.name} does not have getter.")
Expand Down Expand Up @@ -77,7 +98,14 @@ internal sealed class BoundParameterForMap<S> {
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)
)
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/main/kotlin/com/mapk/kmapper/KMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ class KMapper<T : Any> private constructor(

private val parameterMap: Map<String, ParameterForMap<*>> = 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<KClass<*>, List<ArgumentBinder>> = ConcurrentHashMap()

Expand Down
24 changes: 20 additions & 4 deletions src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.isSuperclassOf

internal class ParameterForMap<T : Any> private constructor(val param: KParameter, private val clazz: KClass<T>) {
internal class ParameterForMap<T : Any> private constructor(
val param: KParameter,
private val clazz: KClass<T>,
private val parameterNameConverter: (String) -> String
) {
private val javaClazz: Class<T> by lazy {
clazz.java
}
Expand Down Expand Up @@ -38,15 +42,18 @@ internal class ParameterForMap<T : Any> 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)
}
}
}
Expand All @@ -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<T : Any>(private val boundKMapper: BoundKMapper<T, *>) : 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)
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ private fun <T : Any> convertersFromCompanionObject(clazz: KClass<T>): Set<Pair<
// 引数の型がconverterに対して入力可能ならconverterを返す
internal fun <T : Any> Set<Pair<KClass<*>, KFunction<T>>>.getConverter(input: KClass<out T>): KFunction<T>? =
this.find { (key, _) -> input.isSubclassOf(key) }?.second

// 再帰的マッピング時にKMapperでマップする場合、引数の数が1つだと正常にマッピングが機能しないため、2引数にするために用いるダミー
internal val PARAMETER_DUMMY = "" to null
5 changes: 4 additions & 1 deletion src/main/kotlin/com/mapk/kmapper/PlainKMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ class PlainKMapper<T : Any> private constructor(

private val parameterMap: Map<String, PlainParameterForMap<*>> = 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 ->
Expand Down
13 changes: 9 additions & 4 deletions src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.isSuperclassOf

internal class PlainParameterForMap<T : Any> private constructor(val param: KParameter, private val clazz: KClass<T>) {
internal class PlainParameterForMap<T : Any> private constructor(
val param: KParameter,
private val clazz: KClass<T>,
private val parameterNameConverter: (String) -> String
) {
private val javaClazz: Class<T> by lazy {
clazz.java
}
Expand All @@ -28,13 +32,14 @@ internal class PlainParameterForMap<T : Any> 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)
}
}
}
119 changes: 119 additions & 0 deletions src/test/kotlin/com/mapk/kmapper/RecursiveMappingTest.kt
Original file line number Diff line number Diff line change
@@ -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<String, Int>
)
private data class InnerSnakeSrc(
val hoge_hoge: Int,
val fuga_fuga: Short,
val piyo_piyo: String,
val moge_moge: Pair<String, Int>
)

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<String, Any>, 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)
}
}
}