diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index bc8d0a3..38167d7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 6073570..166966d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,12 @@ plugins { id("maven") id("java") - id("org.jetbrains.kotlin.jvm") version "1.3.71" + id("org.jetbrains.kotlin.jvm") version "1.3.72" id("org.jlleitschuh.gradle.ktlint") version "9.2.1" } group = "com.mapk" -version = "0.22" +version = "0.23" java { sourceCompatibility = JavaVersion.VERSION_1_8 @@ -18,7 +18,7 @@ buildscript { } dependencies { - classpath(kotlin("gradle-plugin", version = "1.3.71")) + classpath(kotlin("gradle-plugin")) } } @@ -30,10 +30,10 @@ repositories { dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation(kotlin("reflect")) - api("com.github.ProjectMapK:Shared:0.10") + api("com.github.ProjectMapK:Shared:0.11") // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter - testImplementation(group = "org.junit.jupiter", name = "junit-jupiter", version = "5.6.1") { + testImplementation(group = "org.junit.jupiter", name = "junit-jupiter", version = "5.6.2") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } // 現状プロパティ名の変換はテストでしか使っていないのでtestImplementation diff --git a/src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt b/src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt index d487b30..d9b6e7e 100644 --- a/src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt +++ b/src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt @@ -20,7 +20,7 @@ import kotlin.reflect.jvm.jvmName class BoundKMapper private constructor( private val function: KFunctionForCall, src: KClass, - parameterNameConverter: (String) -> String = { it } + parameterNameConverter: (String) -> String ) { constructor(function: KFunction, src: KClass, parameterNameConverter: (String) -> String = { it }) : this( KFunctionForCall(function), src, parameterNameConverter @@ -45,7 +45,7 @@ class BoundKMapper private constructor( .filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() } .mapNotNull { val temp = srcPropertiesMap[parameterNameConverter(it.getAliasOrName()!!)]?.let { property -> - BoundParameterForMap(it, property) + BoundParameterForMap.newInstance(it, property) } // 必須引数に対応するプロパティがsrcに定義されていない場合エラー diff --git a/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt b/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt index 03d0d96..0f954ab 100644 --- a/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt +++ b/src/main/kotlin/com/mapk/kmapper/BoundParameterForMap.kt @@ -2,41 +2,83 @@ package com.mapk.kmapper import com.mapk.core.EnumMapper import java.lang.IllegalArgumentException +import java.lang.reflect.Method import kotlin.reflect.KClass +import kotlin.reflect.KFunction import kotlin.reflect.KParameter import kotlin.reflect.KProperty1 import kotlin.reflect.full.isSubclassOf import kotlin.reflect.jvm.javaGetter @Suppress("UNCHECKED_CAST") -internal class BoundParameterForMap(val param: KParameter, property: KProperty1) { - val map: (S) -> Any? +internal sealed class BoundParameterForMap { + abstract val param: KParameter + protected abstract val propertyGetter: Method - init { - // ゲッターが無いならエラー - val propertyGetter = property.javaGetter - ?: throw IllegalArgumentException("${property.name} does not have getter.") - propertyGetter.isAccessible = true + abstract fun map(src: S): Any? - val paramClazz = param.type.classifier as KClass<*> - val propertyClazz = property.returnType.classifier as KClass<*> + private class Plain( + override val param: KParameter, + override val propertyGetter: Method + ) : BoundParameterForMap() { + override fun map(src: S): Any? = propertyGetter.invoke(src) + } + + private class UseConverter( + override val param: KParameter, + override val propertyGetter: Method, + private val converter: KFunction<*> + ) : BoundParameterForMap() { + override fun map(src: S): Any? = converter.call(propertyGetter.invoke(src)) + } + + private class ToEnum( + override val param: KParameter, + override val propertyGetter: Method, + private val paramClazz: Class<*> + ) : BoundParameterForMap() { + override fun map(src: S): Any? = EnumMapper.getEnum(paramClazz, propertyGetter.invoke(src) as String) + } + + private class ToString( + override val param: KParameter, + override val propertyGetter: Method + ) : BoundParameterForMap() { + override fun map(src: S): String? = propertyGetter.invoke(src).toString() + } + + companion object { + fun newInstance(param: KParameter, property: KProperty1): BoundParameterForMap { + // ゲッターが無いならエラー + val propertyGetter = property.javaGetter + ?: throw IllegalArgumentException("${property.name} does not have getter.") + propertyGetter.isAccessible = true + + val paramClazz = param.type.classifier as KClass<*> + val propertyClazz = property.returnType.classifier as KClass<*> - val converter = paramClazz.getConverters() - .filter { (key, _) -> propertyClazz.isSubclassOf(key) } - .let { - if (1 < it.size) throw IllegalArgumentException("${param.name} has multiple converter. $it") + // コンバータが取れた場合 + paramClazz.getConverters() + .filter { (key, _) -> propertyClazz.isSubclassOf(key) } + .let { + if (1 < it.size) throw IllegalArgumentException("${param.name} has multiple converter. $it") - it.singleOrNull()?.second + it.singleOrNull()?.second + }?.let { + return UseConverter(param, propertyGetter, it) + } + + if (paramClazz.isSubclassOf(propertyClazz)) { + return Plain(param, propertyGetter) } - map = when { - converter != null -> { { converter.call(propertyGetter.invoke(it)) } } - paramClazz.isSubclassOf(propertyClazz) -> { { propertyGetter.invoke(it) } } - paramClazz.java.isEnum && propertyClazz == String::class -> { { - EnumMapper.getEnum(paramClazz.java, propertyGetter.invoke(it) as String) - } } - paramClazz == String::class -> { { propertyGetter.invoke(it).toString() } } - else -> throw IllegalArgumentException("Can not convert $propertyClazz to $paramClazz") + val javaClazz = paramClazz.java + + 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") + } } } } diff --git a/src/main/kotlin/com/mapk/kmapper/KMapper.kt b/src/main/kotlin/com/mapk/kmapper/KMapper.kt index 910cead..4faff9a 100644 --- a/src/main/kotlin/com/mapk/kmapper/KMapper.kt +++ b/src/main/kotlin/com/mapk/kmapper/KMapper.kt @@ -33,7 +33,7 @@ class KMapper private constructor( .filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() } .associate { (parameterNameConverter(it.getAliasOrName()!!)) to ParameterForMap.newInstance(it) } - private val getCache: ConcurrentMap, List<(Any, ArgumentBucket) -> Unit>> = ConcurrentHashMap() + private val getCache: ConcurrentMap, List> = ConcurrentHashMap() private fun bindArguments(argumentBucket: ArgumentBucket, src: Any) { val clazz = src::class @@ -41,11 +41,11 @@ class KMapper private constructor( // キャッシュヒットしたら登録した内容に沿って取得処理を行う getCache[clazz]?.let { getters -> // 取得対象フィールドは十分絞り込んでいると考えられるため、終了判定は行わない - getters.forEach { it(src, argumentBucket) } + getters.forEach { it.bindArgument(src, argumentBucket) } return } - val tempCacheArrayList = ArrayList<(Any, ArgumentBucket) -> Unit>() + val tempBinderArrayList = ArrayList() src::class.memberProperties.forEach outer@{ property -> // propertyが公開されていない場合は処理を行わない @@ -64,19 +64,14 @@ class KMapper private constructor( parameterMap[alias ?: property.name]?.let { param -> javaGetter.isAccessible = true - val tempCache = { value: Any, bucket: ArgumentBucket -> - // 初期化済みであれば高コストな取得処理は行わない - if (!bucket.containsKey(param.param)) { - // javaGetterを呼び出す方が高速 - bucket.putIfAbsent(param.param, javaGetter.invoke(value)?.let { param.mapObject(it) }) - } - } - tempCache(src, argumentBucket) - tempCacheArrayList.add(tempCache) + val binder = ArgumentBinder(param, javaGetter) + + binder.bindArgument(src, argumentBucket) + tempBinderArrayList.add(binder) // キャッシュの整合性を保つため、ここでは終了判定を行わない } } - getCache.putIfAbsent(clazz, tempCacheArrayList) + getCache.putIfAbsent(clazz, tempBinderArrayList) } private fun bindArguments(argumentBucket: ArgumentBucket, src: Map<*, *>) { @@ -131,3 +126,13 @@ class KMapper private constructor( return function.call(bucket) } } + +private class ArgumentBinder(private val param: ParameterForMap<*>, private val javaGetter: Method) { + fun bindArgument(value: Any, bucket: ArgumentBucket) { + // 初期化済みであれば高コストな取得処理は行わない + if (!bucket.containsKey(param.param)) { + // javaGetterを呼び出す方が高速 + bucket.putIfAbsent(param.param, javaGetter.invoke(value)?.let { param.mapObject(it) }) + } + } +} diff --git a/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt b/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt index 25d1875..eceec68 100644 --- a/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt +++ b/src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt @@ -15,33 +15,33 @@ internal class ParameterForMap private constructor(val param: KParamete // リストの長さが小さいと期待されるためこの形で実装しているが、理想的にはmap的なものが使いたい private val converters: Set, KFunction>> = clazz.getConverters() - private val convertCache: ConcurrentMap, (Any) -> Any?> = ConcurrentHashMap() + private val convertCache: ConcurrentMap, ParameterProcessor> = ConcurrentHashMap() fun mapObject(value: U): Any? { val valueClazz: KClass<*> = value::class // 取得方法のキャッシュが有ればそれを用いる - convertCache[valueClazz]?.let { return it(value) } + convertCache[valueClazz]?.let { return it.process(value) } // パラメータに対してvalueが代入可能(同じもしくは親クラス)であればそのまま用いる if (clazz.isSuperclassOf(valueClazz)) { - convertCache.putIfAbsent(valueClazz) { it } + convertCache.putIfAbsent(valueClazz, ParameterProcessor.Plain) return value } val converter: KFunction<*>? = converters.getConverter(valueClazz) - val lambda: (Any) -> Any? = when { + val processor: ParameterProcessor = when { // converterに一致する組み合わせが有れば設定されていればそれを使う - converter != null -> { { converter.call(it) } } + converter != null -> ParameterProcessor.UseConverter(converter) // 要求された値がenumかつ元が文字列ならenum mapperでマップ - javaClazz.isEnum && value is String -> { { EnumMapper.getEnum(javaClazz, it as String) } } + javaClazz.isEnum && value is String -> ParameterProcessor.ToEnum(javaClazz) // 要求されているパラメータがStringならtoStringする - clazz == String::class -> { { it.toString() } } + clazz == String::class -> ParameterProcessor.ToString else -> throw IllegalArgumentException("Can not convert $valueClazz to $clazz") } - convertCache.putIfAbsent(valueClazz, lambda) - return lambda(value) + convertCache.putIfAbsent(valueClazz, processor) + return processor.process(value) } companion object { @@ -50,3 +50,23 @@ internal class ParameterForMap private constructor(val param: KParamete } } } + +private sealed class ParameterProcessor { + abstract fun process(value: Any): Any? + + object Plain : ParameterProcessor() { + override fun process(value: Any): Any? = value + } + + class UseConverter(private val converter: KFunction<*>) : ParameterProcessor() { + override fun process(value: Any): Any? = converter.call(value) + } + + class ToEnum(private val javaClazz: Class<*>) : ParameterProcessor() { + override fun process(value: Any): Any? = EnumMapper.getEnum(javaClazz, value as String) + } + + object ToString : ParameterProcessor() { + override fun process(value: Any): Any? = value.toString() + } +}