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
23 commits
Select commit Hold shift + click to select a range
b3fba0f
バージョンアップ
k163377 Apr 4, 2020
029b43e
Converterの取得関連をutil化
k163377 Apr 4, 2020
efbb2c1
とりあえず動くところまで実装
k163377 Apr 4, 2020
2576baf
BoundKMapperの基本的なテストを追加
k163377 Apr 4, 2020
78dfa22
Enum/Stringへの変換まで対応
k163377 Apr 4, 2020
2c3dac9
BoundKMapperのデフォルト値対応テストを追加
k163377 Apr 4, 2020
b093653
デフォルト値利用の実装漏れを修正
k163377 Apr 4, 2020
541faaf
BoundKMapperのEnumマッピングテストを追加
k163377 Apr 4, 2020
16f9e19
invokeし忘れていたため修正
k163377 Apr 4, 2020
f6297e4
BoundKMapperでKGetterIgnoreのテストを追加
k163377 Apr 5, 2020
d64e2ee
無駄に大きかったインデントを修正
k163377 Apr 5, 2020
7df7967
data classに修正
k163377 Apr 5, 2020
6af91e6
BoundKMapperでパラメータ名変換のテストを追加
k163377 Apr 5, 2020
41cbae6
BoundKMapperでパラメータ名とゲッターそれぞれにエイリアスを貼った場合のテストを追加
k163377 Apr 5, 2020
c0afef5
コメント修正
k163377 Apr 5, 2020
18b16ba
BoundKMapperで文字列に対してtoStringしたものを渡すテストを追加
k163377 Apr 5, 2020
6cbcfca
コンバータのサポートを追加
k163377 Apr 5, 2020
2332a7d
BoundKMapperでのコンバーター利用テストを追加
k163377 Apr 5, 2020
02d88d1
propertyのままになっていた部分を修正
k163377 Apr 5, 2020
3db4c67
classからの宣言を追加
k163377 Apr 5, 2020
1c2b33b
マッパーをパラメタライズテストで順次与える方式に修正
k163377 Apr 5, 2020
8a008ad
unused警告を抑制
k163377 Apr 5, 2020
53b8324
paramとpropertyを間違えており正常に文字列へのマッピングができていなかったため修正
k163377 Apr 5, 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.19"
version = "0.20"

java {
sourceCompatibility = JavaVersion.VERSION_1_8
Expand Down
69 changes: 69 additions & 0 deletions src/main/kotlin/com/mapk/kmapper/BoundKMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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.IllegalArgumentException
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.KVisibility
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.jvmName

class BoundKMapper<S : Any, D : Any> private constructor(
private val function: KFunctionForCall<D>,
src: KClass<S>,
parameterNameConverter: (String) -> String = { it }
) {
constructor(function: KFunction<D>, src: KClass<S>, parameterNameConverter: (String) -> String = { it }) : this(
KFunctionForCall(function), src, parameterNameConverter
)

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

private val parameters: List<BoundParameterForMap<S>>

init {
val srcPropertiesMap: Map<String, KProperty1<S, *>> =
src.memberProperties
.filter {
// アクセス可能かつignoreされてないもののみ抽出
!(it.visibility != KVisibility.PUBLIC) &&
it.getter.annotations.none { annotation -> annotation is KGetterIgnore }
}.associateBy { it.getter.findAnnotation<KGetterAlias>()?.value ?: it.name }

parameters = function.parameters
.filter { it.kind != KParameter.Kind.INSTANCE && !it.isUseDefaultArgument() }
.mapNotNull {
val temp = srcPropertiesMap[parameterNameConverter(it.getAliasOrName()!!)]?.let { property ->
BoundParameterForMap(it, property)
}

// 必須引数に対応するプロパティがsrcに定義されていない場合エラー
if (temp == null && !it.isOptional) {
throw IllegalArgumentException("Property ${it.name!!} is not declared in ${src.jvmName}.")
}

temp
}
}

fun map(src: S): D {
val bucket: ArgumentBucket = function.getArgumentBucket()

parameters.forEach {
bucket.putIfAbsent(it.param, it.map(src))
}

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

import com.mapk.core.EnumMapper
import java.lang.IllegalArgumentException
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.jvm.javaGetter

@Suppress("UNCHECKED_CAST")
internal class BoundParameterForMap<S : Any>(val param: KParameter, property: KProperty1<S, *>) {
val map: (S) -> Any?

init {
// ゲッターが無いならエラー
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 = (convertersFromConstructors(paramClazz) +
convertersFromStaticMethods(paramClazz) +
convertersFromCompanionObject(paramClazz))
.filter { (key, _) -> propertyClazz.isSubclassOf(key) }
.let {
if (1 < it.size) throw IllegalArgumentException("${param.name} has multiple converter. $it")

it.singleOrNull()?.second
}

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")
}
}
}
42 changes: 0 additions & 42 deletions src/main/kotlin/com/mapk/kmapper/ParameterForMap.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
package com.mapk.kmapper

import com.mapk.annotations.KConverter
import com.mapk.core.KFunctionWithInstance
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
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 class ParameterForMap<T : Any> private constructor(val param: KParameter, val clazz: KClass<T>) {
val javaClazz: Class<T> by lazy {
Expand All @@ -30,39 +24,3 @@ internal class ParameterForMap<T : Any> private constructor(val param: KParamete
}
}
}

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

(func.parameters.single().type.classifier as KClass<*>) to func
}.toSet()
}

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

@Suppress("UNCHECKED_CAST")
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")
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 } }
.map { function ->
val func: KFunction<T> = KFunctionWithInstance(
function,
companionObject
) as KFunction<T>

(func.parameters.single().type.classifier as KClass<*>) to func
}.toSet()
} ?: emptySet()
}
46 changes: 46 additions & 0 deletions src/main/kotlin/com/mapk/kmapper/ParameterUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.mapk.kmapper

import com.mapk.annotations.KConverter
import com.mapk.core.KFunctionWithInstance
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.companionObjectInstance
import kotlin.reflect.full.functions
import kotlin.reflect.full.staticFunctions
import kotlin.reflect.jvm.isAccessible

internal 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

(func.parameters.single().type.classifier as KClass<*>) to func
}.toSet()
}

internal 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>>> {
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>>> {
return clazz.companionObjectInstance?.let { companionObject ->
companionObject::class.functions
.filter { it.annotations.any { annotation -> annotation is KConverter } }
.map { function ->
val func: KFunction<T> = KFunctionWithInstance(
function,
companionObject
) as KFunction<T>

(func.parameters.single().type.classifier as KClass<*>) to func
}.toSet()
} ?: emptySet()
}
35 changes: 35 additions & 0 deletions src/test/kotlin/com/mapk/kmapper/ConverterKMapperTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class CompanionConverter private constructor(val arg: String) {

private data class StaticMethodConverterDst(val argument: StaticMethodConverter)

private data class BoundConstructorConverterSrc(val argument: Int)
private data class BoundCompanionConverterSrc(val argument: String)
private data class BoundStaticMethodConverterSrc(val argument: String)

@DisplayName("コンバータ有りでのマッピングテスト")
class ConverterKMapperTest {
@Nested
Expand Down Expand Up @@ -57,4 +61,35 @@ class ConverterKMapperTest {
assertTrue(intArrayOf(1, 2, 3) contentEquals result.argument.arg)
}
}

@Nested
@DisplayName("BoundKMapper")
inner class BoundKMapperTest {
@Test
@DisplayName("コンストラクターでのコンバートテスト")
fun constructorConverterTest() {
val mapper = BoundKMapper(::ConstructorConverterDst, BoundConstructorConverterSrc::class)
val result = mapper.map(BoundConstructorConverterSrc(1))

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

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

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

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

assertTrue(intArrayOf(1, 2, 3) contentEquals result.argument.arg)
}
}
}
14 changes: 12 additions & 2 deletions src/test/kotlin/com/mapk/kmapper/DefaultArgumentTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,25 @@ class DefaultArgumentTest {
data class Dst(val fooArgument: Int, @param:KUseDefaultArgument val barArgument: String = "default")
data class Src(val fooArgument: Int, val barArgument: String)

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

@Nested
@DisplayName("KMapper")
inner class KMapperTest {
@Test
fun test() {
val src = Src(1, "src")

val result = KMapper(::Dst).map(src)
assertEquals(Dst(1, "default"), result)
}
}

@Nested
@DisplayName("BoundKMapper")
inner class BoundKMapperTest {
@Test
fun test() {
val result = BoundKMapper(::Dst, Src::class).map(src)
assertEquals(Dst(1, "default"), result)
}
}
}
16 changes: 16 additions & 0 deletions src/test/kotlin/com/mapk/kmapper/EnumMappingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,20 @@ class EnumMappingTest {
assertEquals(language, result.language)
}
}

data class BoundSrc(val language: String)

@Nested
@DisplayName("BoundKMapper")
inner class BoundKMapperTest {
private val mapper = BoundKMapper(::EnumMappingDst, BoundSrc::class)

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

assertEquals(language, result.language)
}
}
}
17 changes: 17 additions & 0 deletions src/test/kotlin/com/mapk/kmapper/KGetterIgnoreTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,21 @@ class KGetterIgnoreTest {
assertEquals(Dst(1, "2-1", 32, "4"), dst1)
}
}

data class BoundSrc(val arg1: Int, val arg2: Short, @get:KGetterIgnore val arg3: String)
data class BoundDst(val arg1: Int, val arg2: Short, val arg3: String = "default")

@Nested
@DisplayName("BoundKMapper")
inner class BoundKMapperTest {
@Test
@DisplayName("フィールドを無視するテスト")
fun test() {
val mapper = BoundKMapper(::BoundDst, BoundSrc::class)

val result = mapper.map(BoundSrc(1, 2, "arg3"))

assertEquals(BoundDst(1, 2, "default"), result)
}
}
}
24 changes: 19 additions & 5 deletions src/test/kotlin/com/mapk/kmapper/ParameterNameConverterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test

private class CamelCaseDst(val camelCase: String)
private data class CamelCaseDst(val camelCase: String)
private data class BoundSrc(val camel_case: String)

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

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

assertEquals(expected, result.camelCase)
}
}

@Nested
@DisplayName("BoundKMapper")
inner class BoundKMapperTest {
@Test
@DisplayName("スネークケースsrc -> キャメルケースdst")
fun test() {

val mapper = BoundKMapper(::CamelCaseDst, BoundSrc::class) {
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, it)
}
val result = mapper.map(BoundSrc("snakeCase"))

assertEquals(CamelCaseDst("snakeCase"), result)
}
}
}
Loading