diff --git a/build.gradle.kts b/build.gradle.kts index 0c252ad..3c12044 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.mapk" -version = "0.1" +version = "0.6" java { sourceCompatibility = JavaVersion.VERSION_1_8 @@ -29,6 +29,8 @@ repositories { dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation(kotlin("reflect")) + // https://mvnrepository.com/artifact/org.jetbrains/annotations + implementation(group = "org.jetbrains", name = "annotations", version = "19.0.0") // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter testImplementation(group = "org.junit.jupiter", name = "junit-jupiter", version = "5.6.0") { diff --git a/src/main/java/com/mapk/core/BucketGenerator.java b/src/main/java/com/mapk/core/BucketGenerator.java index b74eda7..0e909d5 100644 --- a/src/main/java/com/mapk/core/BucketGenerator.java +++ b/src/main/java/com/mapk/core/BucketGenerator.java @@ -2,19 +2,24 @@ import kotlin.Pair; import kotlin.reflect.KParameter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; class BucketGenerator { private final int initializationStatus; + @NotNull private final List initializeMask; private final int completionValue; + @NotNull private final KParameter[] keyArray; + @NotNull private final Object[] valueArray; - BucketGenerator(int capacity, Pair instancePair) { + BucketGenerator(int capacity, @Nullable Pair instancePair) { keyArray = new KParameter[capacity]; valueArray = new Object[capacity]; @@ -38,6 +43,7 @@ class BucketGenerator { this.completionValue = completionValue; } + @NotNull ArgumentBucket generate() { return new ArgumentBucket( keyArray.clone(), diff --git a/src/main/java/com/mapk/core/EnumMapper.java b/src/main/java/com/mapk/core/EnumMapper.java index d5074ad..e206016 100644 --- a/src/main/java/com/mapk/core/EnumMapper.java +++ b/src/main/java/com/mapk/core/EnumMapper.java @@ -1,5 +1,8 @@ package com.mapk.core; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + public class EnumMapper { /** * Kotlinの型推論バグでクラスからvalueOfが使えないため、ここだけJavaで書いている(型引数もT extends Enumでは書けなかった) @@ -8,8 +11,9 @@ public class EnumMapper { * @param enumClass * @return Enum.valueOf */ + @Nullable @SuppressWarnings({"unchecked", "rawtypes"}) - public static T getEnum(Class clazz, String value) { + public static T getEnum(@NotNull Class clazz, @Nullable String value) { if (value == null || value.isEmpty()) { return null; } diff --git a/src/main/kotlin/com/mapk/annotations/KConverter.kt b/src/main/kotlin/com/mapk/annotations/KConstructor.kt similarity index 83% rename from src/main/kotlin/com/mapk/annotations/KConverter.kt rename to src/main/kotlin/com/mapk/annotations/KConstructor.kt index 35034f7..6ea3720 100644 --- a/src/main/kotlin/com/mapk/annotations/KConverter.kt +++ b/src/main/kotlin/com/mapk/annotations/KConstructor.kt @@ -3,4 +3,4 @@ package com.mapk.annotations @Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) @MustBeDocumented -annotation class KConverter +annotation class KConstructor diff --git a/src/main/kotlin/com/mapk/core/ArgumentBucket.kt b/src/main/kotlin/com/mapk/core/ArgumentBucket.kt index d148cff..266812d 100644 --- a/src/main/kotlin/com/mapk/core/ArgumentBucket.kt +++ b/src/main/kotlin/com/mapk/core/ArgumentBucket.kt @@ -1,5 +1,6 @@ package com.mapk.core +import java.util.Objects import kotlin.reflect.KParameter class ArgumentBucket internal constructor( @@ -26,9 +27,7 @@ class ArgumentBucket internal constructor( return keyArray[key.index] != null } - override fun containsValue(value: Any?): Boolean { - throw UnsupportedOperationException() - } + override fun containsValue(value: Any?): Boolean = valueArray.any { Objects.equals(value, it) } override fun get(key: KParameter): Any? = valueArray[key.index] fun getByIndex(key: Int): Any? = @@ -42,7 +41,7 @@ class ArgumentBucket internal constructor( override val keys: MutableSet get() = keyArray.filterNotNull().toMutableSet() override val values: MutableCollection - get() = throw UnsupportedOperationException() + get() = valueArray.filterIndexed { i, _ -> initializationStatus and initializeMask[i] != 0 }.toMutableList() fun putIfAbsent(key: KParameter, value: Any?) { val index = key.index diff --git a/src/main/kotlin/com/mapk/core/Functions.kt b/src/main/kotlin/com/mapk/core/Functions.kt new file mode 100644 index 0000000..ef29771 --- /dev/null +++ b/src/main/kotlin/com/mapk/core/Functions.kt @@ -0,0 +1,10 @@ +package com.mapk.core + +import com.mapk.annotations.KParameterAlias +import kotlin.reflect.KParameter +import kotlin.reflect.full.findAnnotation + +/** + * パラメータからエイリアスもしくはプロパティ名を取得する関数 + */ +fun KParameter.getAliasOrName(): String? = findAnnotation()?.value ?: name diff --git a/src/main/kotlin/com/mapk/core/KFunctionForCall.kt b/src/main/kotlin/com/mapk/core/KFunctionForCall.kt index 33b8a3f..51fa697 100644 --- a/src/main/kotlin/com/mapk/core/KFunctionForCall.kt +++ b/src/main/kotlin/com/mapk/core/KFunctionForCall.kt @@ -1,10 +1,15 @@ package com.mapk.core +import com.mapk.annotations.KConstructor +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.primaryConstructor import kotlin.reflect.jvm.isAccessible -class KFunctionForCall(private val function: KFunction, instance: Any? = null) { +class KFunctionForCall(internal val function: KFunction, instance: Any? = null) { val parameters: List = function.parameters private val generator: BucketGenerator @@ -28,3 +33,23 @@ class KFunctionForCall(private val function: KFunction, instance: Any? = n if (argumentBucket.isInitialized) function.call(*argumentBucket.valueArray) else function.callBy(argumentBucket) } + +@Suppress("UNCHECKED_CAST") +fun KClass.toKConstructor(): KFunctionForCall { + val factoryConstructor: List> = + this.companionObjectInstance?.let { companionObject -> + companionObject::class.functions + .filter { it.annotations.any { annotation -> annotation is KConstructor } } + .map { KFunctionForCall(it, companionObject) as KFunctionForCall } + } ?: emptyList() + + val constructors: List> = factoryConstructor + this.constructors + .filter { it.annotations.any { annotation -> annotation is KConstructor } } + .map { KFunctionForCall(it) } + + if (constructors.size == 1) return constructors.single() + + if (constructors.isEmpty()) return KFunctionForCall(this.primaryConstructor!!) + + throw IllegalArgumentException("Find multiple target.") +} diff --git a/src/test/kotlin/com/mapk/core/ToKConstructorTest.kt b/src/test/kotlin/com/mapk/core/ToKConstructorTest.kt new file mode 100644 index 0000000..375633d --- /dev/null +++ b/src/test/kotlin/com/mapk/core/ToKConstructorTest.kt @@ -0,0 +1,68 @@ +package com.mapk.core + +import com.mapk.annotations.KConstructor +import kotlin.reflect.KFunction +import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.isAccessible +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +@Suppress("UNCHECKED_CAST", "unused") +@DisplayName("クラスからのコンストラクタ抽出関連テスト") +class ToKConstructorTest { + private class SecondaryConstructorDst(val argument: Int) { + @KConstructor + constructor(argument: Number) : this(argument.toInt()) + } + + class CompanionFactoryDst(val argument: IntArray) { + companion object { + @KConstructor + fun factory(csv: String): CompanionFactoryDst { + return csv.split(",").map { it.toInt() }.toIntArray().let { CompanionFactoryDst(it) } + } + } + } + private class ConstructorDst(val argument: String) + class MultipleConstructorDst @KConstructor constructor(val argument: Int) { + @KConstructor + constructor(argument: String) : this(argument.toInt()) + } + + private fun KFunctionForCall.getTargetFunction(): KFunction { + return this::class.memberProperties.first { it.name == "function" }.getter.let { + it.isAccessible = true + it.call(this) as KFunction + } + } + + @Test + @DisplayName("セカンダリコンストラクタからの取得テスト") + fun testGetFromSecondaryConstructor() { + val function = SecondaryConstructorDst::class.toKConstructor().function + Assertions.assertTrue(function.annotations.any { it is KConstructor }) + } + + @Test + @DisplayName("ファクトリーメソッドからの取得テスト") + fun testGetFromFactoryMethod() { + val function = CompanionFactoryDst::class.toKConstructor().function + Assertions.assertTrue(function.annotations.any { it is KConstructor }) + } + + @Test + @DisplayName("無指定でプライマリコンストラクタからの取得テスト") + fun testGetFromPrimaryConstructor() { + val function = ConstructorDst::class.toKConstructor().function + Assertions.assertEquals(ConstructorDst::class.primaryConstructor, function) + } + + @Test + @DisplayName("対象を複数指定した場合のテスト") + fun testMultipleDeclareConstructor() { + assertThrows { MultipleConstructorDst::class.toKConstructor() } + } +}