Skip to content
This repository was archived by the owner on Jan 20, 2023. It is now read-only.
Merged
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.20"
version = "0.21"

java {
sourceCompatibility = JavaVersion.VERSION_1_8
Expand Down
4 changes: 1 addition & 3 deletions src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ internal class BoundParameterForMap<S : Any>(val param: KParameter, property: KP
val paramClazz = param.type.classifier as KClass<*>
val propertyClazz = property.returnType.classifier as KClass<*>

val converter = (convertersFromConstructors(paramClazz) +
convertersFromStaticMethods(paramClazz) +
convertersFromCompanionObject(paramClazz))
val converter = paramClazz.getConverters()
.filter { (key, _) -> propertyClazz.isSubclassOf(key) }
.let {
if (1 < it.size) throw IllegalArgumentException("${param.name} has multiple converter. $it")
Expand Down
110 changes: 110 additions & 0 deletions src/main/kotlin/com/mapk/kmapper/KMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.mapk.kmapper

import com.mapk.annotations.KGetterAlias
import com.mapk.annotations.KGetterIgnore
import com.mapk.core.ArgumentBucket
import com.mapk.core.KFunctionForCall
import com.mapk.core.getAliasOrName
import com.mapk.core.isUseDefaultArgument
import com.mapk.core.toKConstructor
import java.lang.reflect.Method
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KVisibility
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaGetter

class KMapper<T : Any> private constructor(
private val function: KFunctionForCall<T>,
parameterNameConverter: (String) -> String
) {
constructor(function: KFunction<T>, parameterNameConverter: (String) -> String = { it }) : this(
KFunctionForCall(function), parameterNameConverter
)

constructor(clazz: KClass<T>, parameterNameConverter: (String) -> String = { it }) : this(
clazz.toKConstructor(), parameterNameConverter
)

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

private fun bindArguments(argumentBucket: ArgumentBucket, src: Any) {
src::class.memberProperties.forEach outer@{ property ->
// propertyが公開されていない場合は処理を行わない
if (property.visibility != KVisibility.PUBLIC) return@outer

// ゲッターが取れない場合は処理を行わない
val javaGetter: Method = property.javaGetter ?: return@outer

var alias: String? = null
// NOTE: IgnoreとAliasが同時に指定されるようなパターンを考慮してaliasが取れてもbreakしていない
javaGetter.annotations.forEach {
if (it is KGetterIgnore) return@outer // ignoreされている場合は処理を行わない
if (it is KGetterAlias) alias = it.value
}

parameterMap[alias ?: property.name]?.let {
// javaGetterを呼び出す方が高速
javaGetter.isAccessible = true
argumentBucket.putIfAbsent(it.param, javaGetter.invoke(src)?.let { value -> it.mapObject(value) })
// 終了判定
if (argumentBucket.isInitialized) return
}
}
}

private fun bindArguments(argumentBucket: ArgumentBucket, src: Map<*, *>) {
src.forEach { (key, value) ->
parameterMap[key]?.let { param ->
// 取得した内容がnullでなければ適切にmapする
argumentBucket.putIfAbsent(param.param, value?.let { param.mapObject(value) })
// 終了判定
if (argumentBucket.isInitialized) return
}
}
}

private fun bindArguments(argumentBucket: ArgumentBucket, srcPair: Pair<*, *>) {
parameterMap[srcPair.first.toString()]?.let {
argumentBucket.putIfAbsent(it.param, srcPair.second?.let { value -> it.mapObject(value) })
}
}

fun map(srcMap: Map<String, Any?>): T {
val bucket: ArgumentBucket = function.getArgumentBucket()
bindArguments(bucket, srcMap)

return function.call(bucket)
}

fun map(srcPair: Pair<String, Any?>): T {
val bucket: ArgumentBucket = function.getArgumentBucket()
bindArguments(bucket, srcPair)

return function.call(bucket)
}

fun map(src: Any): T {
val bucket: ArgumentBucket = function.getArgumentBucket()
bindArguments(bucket, src)

return function.call(bucket)
}

fun map(vararg args: Any): T {
val bucket: ArgumentBucket = function.getArgumentBucket()

listOf(*args).forEach { arg ->
when (arg) {
is Map<*, *> -> bindArguments(bucket, arg)
is Pair<*, *> -> bindArguments(bucket, arg)
else -> bindArguments(bucket, arg)
}
}

return function.call(bucket)
}
}
50 changes: 50 additions & 0 deletions src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.mapk.kmapper

import com.mapk.core.EnumMapper
import kotlin.reflect.KClass
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>) {
private val javaClazz: Class<T> by lazy {
clazz.java
}
// リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい
private val converters: Set<Pair<KClass<*>, KFunction<T>>> = clazz.getConverters()

private val convertCache: MutableMap<KClass<*>, (Any) -> Any?> = HashMap()

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

// 取得方法のキャッシュが有ればそれを用いる
convertCache[valueClazz]?.let { return it(value) }

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

val converter: KFunction<*>? = converters.getConverter(valueClazz)

val lambda: (Any) -> Any? = when {
// converterに一致する組み合わせが有れば設定されていればそれを使う
converter != null -> { { converter.call(it) } }
// 要求された値がenumかつ元が文字列ならenum mapperでマップ
javaClazz.isEnum && value is String -> { { EnumMapper.getEnum(javaClazz, it as String) } }
// 要求されているパラメータがStringならtoStringする
clazz == String::class -> { { it.toString() } }
else -> throw IllegalArgumentException("Can not convert $valueClazz to $clazz")
}
convertCache[valueClazz] = lambda
return lambda(value)
}

companion object {
fun newInstance(param: KParameter): ParameterForMap<*> {
return ParameterForMap(param, param.type.classifier as KClass<*>)
}
}
}
16 changes: 12 additions & 4 deletions src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.companionObjectInstance
import kotlin.reflect.full.functions
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.staticFunctions
import kotlin.reflect.jvm.isAccessible

internal fun <T> Collection<KFunction<T>>.getConverterMapFromFunctions(): Set<Pair<KClass<*>, KFunction<T>>> {
internal fun <T : Any> KClass<T>.getConverters(): Set<Pair<KClass<*>, KFunction<T>>> =
convertersFromConstructors(this) + convertersFromStaticMethods(this) + convertersFromCompanionObject(this)

private fun <T> Collection<KFunction<T>>.getConverterMapFromFunctions(): Set<Pair<KClass<*>, KFunction<T>>> {
return filter { it.annotations.any { annotation -> annotation is KConverter } }
.map { func ->
func.isAccessible = true
Expand All @@ -18,19 +22,19 @@ internal fun <T> Collection<KFunction<T>>.getConverterMapFromFunctions(): Set<Pa
}.toSet()
}

internal fun <T : Any> convertersFromConstructors(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
private fun <T : Any> convertersFromConstructors(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
return clazz.constructors.getConverterMapFromFunctions()
}

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

return staticFunctions.getConverterMapFromFunctions()
}

@Suppress("UNCHECKED_CAST")
internal fun <T : Any> convertersFromCompanionObject(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
private fun <T : Any> convertersFromCompanionObject(clazz: KClass<T>): Set<Pair<KClass<*>, KFunction<T>>> {
return clazz.companionObjectInstance?.let { companionObject ->
companionObject::class.functions
.filter { it.annotations.any { annotation -> annotation is KConverter } }
Expand All @@ -44,3 +48,7 @@ internal fun <T : Any> convertersFromCompanionObject(clazz: KClass<T>): Set<Pair
}.toSet()
} ?: emptySet()
}

// 引数の型が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
11 changes: 2 additions & 9 deletions src/main/kotlin/com/mapk/kmapper/PlainParameterForMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,22 @@ import com.mapk.core.EnumMapper
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.isSuperclassOf

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

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

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

val converter: KFunction<*>? = getConverter(valueClazz)
val converter: KFunction<*>? = converters.getConverter(valueClazz)

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

// 引数の型がconverterに対して入力可能ならconverterを返す
private fun <R : Any> getConverter(input: KClass<out R>): KFunction<T>? =
converters.find { (key, _) -> input.isSubclassOf(key) }?.second

companion object {
fun newInstance(param: KParameter): PlainParameterForMap<*> {
return PlainParameterForMap(param, param.type.classifier as KClass<*>)
Expand Down
31 changes: 31 additions & 0 deletions src/test/kotlin/com/mapk/kmapper/ConverterKMapperTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,37 @@ private data class BoundStaticMethodConverterSrc(val argument: String)

@DisplayName("コンバータ有りでのマッピングテスト")
class ConverterKMapperTest {
@Nested
@DisplayName("KMapper")
inner class KMapperTest {
@Test
@DisplayName("コンストラクターでのコンバートテスト")
fun constructorConverterTest() {
val mapper = KMapper(ConstructorConverterDst::class)
val result = mapper.map(mapOf("argument" to 1))

assertEquals(ConstructorConverter(1), result.argument)
}

@Test
@DisplayName("コンパニオンオブジェクトに定義したコンバータでのコンバートテスト")
fun companionConverterTest() {
val mapper = KMapper(CompanionConverterDst::class)
val result = mapper.map(mapOf("argument" to "arg"))

assertEquals("arg", result.argument.arg)
}

@Test
@DisplayName("スタティックメソッドに定義したコンバータでのコンバートテスト")
fun staticMethodConverterTest() {
val mapper = KMapper(StaticMethodConverterDst::class)
val result = mapper.map(mapOf("argument" to "1,2,3"))

assertTrue(intArrayOf(1, 2, 3) contentEquals result.argument.arg)
}
}

@Nested
@DisplayName("PlainKMapper")
inner class PlainKMapperTest {
Expand Down
10 changes: 10 additions & 0 deletions src/test/kotlin/com/mapk/kmapper/DefaultArgumentTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ class DefaultArgumentTest {

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

@Nested
@DisplayName("KMapper")
inner class KMapperTest {
@Test
fun test() {
val result = KMapper(::Dst).map(src)
assertEquals(Dst(1, "default"), result)
}
}

@Nested
@DisplayName("PlainKMapper")
inner class PlainKMapperTest {
Expand Down
14 changes: 14 additions & 0 deletions src/test/kotlin/com/mapk/kmapper/EnumMappingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ private class EnumMappingDst(val language: JvmLanguage?)

@DisplayName("文字列 -> Enumのマッピングテスト")
class EnumMappingTest {
@Nested
@DisplayName("KMapper")
inner class KMapperTest {
private val mapper = KMapper(EnumMappingDst::class)

@ParameterizedTest(name = "Non-Null要求")
@EnumSource(value = JvmLanguage::class)
fun test(language: JvmLanguage) {
val result = mapper.map("language" to language.name)

assertEquals(language, result.language)
}
}

@Nested
@DisplayName("PlainKMapper")
inner class PlainKMapperTest {
Expand Down
19 changes: 19 additions & 0 deletions src/test/kotlin/com/mapk/kmapper/KGetterIgnoreTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,25 @@ class KGetterIgnoreTest {

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

@Nested
@DisplayName("KMapper")
inner class KMapperTest {
@Test
@DisplayName("フィールドを無視するテスト")
fun test() {
val src1 = Src1(1, "2-1", 31)
val src2 = Src2("2-2", 32, "4")

val mapper = KMapper(::Dst)

val dst1 = mapper.map(src1, src2)
val dst2 = mapper.map(src2, src1)

assertTrue(dst1 == dst2)
assertEquals(Dst(1, "2-1", 32, "4"), dst1)
}
}

@Nested
@DisplayName("PlainKMapper")
inner class PlainKMapperTest {
Expand Down
18 changes: 18 additions & 0 deletions src/test/kotlin/com/mapk/kmapper/ParameterNameConverterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ private data class BoundSrc(val camel_case: String)

@DisplayName("パラメータ名変換のテスト")
class ParameterNameConverterTest {
@Nested
@DisplayName("KMapper")
inner class KMapperTest {
@Test
@DisplayName("スネークケースsrc -> キャメルケースdst")
fun test() {
val expected = "snakeCase"
val src = mapOf("camel_case" to expected)

val mapper = KMapper(CamelCaseDst::class) {
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it)
}
val result = mapper.map(src)

assertEquals(expected, result.camelCase)
}
}

@Nested
@DisplayName("PlainKMapper")
inner class PlainKMapperTest {
Expand Down
Loading