Skip to content

Commit

Permalink
Fix the implementation of equals for qualifiers with arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
ansman committed Aug 18, 2021
1 parent 1badbb1 commit 5baf65b
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 10 deletions.
2 changes: 2 additions & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ plugins {

dependencies {
api(deps.moshi)
testImplementation(deps.junit)
testImplementation(deps.truth)
}
61 changes: 51 additions & 10 deletions api/src/main/kotlin/se/ansman/kotshi/KotshiUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Types
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Proxy
import java.lang.reflect.Type
Expand All @@ -18,11 +20,13 @@ object KotshiUtils {
@JvmStatic
val Type.typeArgumentsOrFail: Array<Type>
get() = (this as? ParameterizedType)?.actualTypeArguments
?: throw IllegalArgumentException("""
?: throw IllegalArgumentException(
"""
${Types.getRawType(this).simpleName} is generic and requires you to specify its type
arguments. Don't request an adapter using Type::class.java or value.javaClass but rather using
Types.newParameterizedType.
""".trimIndent())
""".trimIndent()
)

@JvmStatic
@JvmOverloads
Expand All @@ -40,15 +44,52 @@ object KotshiUtils {
fun <T : Annotation> Class<T>.createJsonQualifierImplementation(annotationArguments: Map<String, Any> = emptyMap()): T {
require(isAnnotation) { "$this must be an annotation." }
@Suppress("UNCHECKED_CAST")
return Proxy.newProxyInstance(classLoader, arrayOf<Class<*>>(this)) { proxy, method, args ->
when (method.name) {
"annotationType" -> this
"equals" -> isInstance(args[0])
"hashCode" -> 0
"toString" -> "@$name()"
else -> annotationArguments[method.name] ?: method.invoke(proxy, *args)
return Proxy.newProxyInstance(classLoader, arrayOf<Class<*>>(this), object : InvocationHandler {
private val annotationMethods = declaredMethods
.onEach { method ->
val value = annotationArguments[method.name]
if (value == null) {
require(method.defaultValue != null) {
"Annotation value for method ${method.name} is missing"
}
} else {
val expectedType = when (val returnType = method.returnType) {
Boolean::class.java -> Boolean::class.javaObjectType
Byte::class.java -> Byte::class.javaObjectType
Char::class.java -> Char::class.javaObjectType
Double::class.java -> Double::class.javaObjectType
Float::class.java -> Float::class.javaObjectType
Int::class.java -> Int::class.javaObjectType
Long::class.java -> Long::class.javaObjectType
Short::class.java -> Short::class.javaObjectType
Void::class.java -> Void::class.javaObjectType
else -> returnType
}
require(expectedType.isInstance(value)) {
"Expected value for method ${method.name} to be of type $expectedType but was ${value.javaClass}"
}
}
}
.toList()

init {
val extraArguments = annotationArguments.keys - annotationMethods.map { it.name }
require(extraArguments.isEmpty()) {
"Found annotation values for unknown methods: $extraArguments"
}
}
} as T

override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any =
when (method.name) {
"annotationType" -> this
"equals" -> args!![0] === proxy || isInstance(args!![0]) && annotationMethods.all { m ->
m.invoke(args[0]) == annotationArguments.getOrDefault(m.name, m.defaultValue)
}
"hashCode" -> annotationArguments.hashCode()
"toString" -> "@$name(${annotationArguments.entries.joinToString()})"
else -> annotationArguments[method.name] ?: method.defaultValue
}
}) as T
}

@JvmStatic
Expand Down
42 changes: 42 additions & 0 deletions api/src/test/kotlin/se/ansman/kotshi/TestKotshiUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package se.ansman.kotshi

import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonQualifier
import org.junit.Test
import se.ansman.kotshi.KotshiUtils.createJsonQualifierImplementation

class TestKotshiUtils {
private val noArgs = TestQualifier::class.java.createJsonQualifierImplementation()
private val withArgs = TestQualifier::class.java.createJsonQualifierImplementation(mapOf("foo" to "bar"))

@Test
fun testCreateJsonQualifierImplementation_equals() {
assertThat(noArgs).isEqualTo(noArgs)
assertThat(noArgs).isEqualTo(TestQualifier::class.java.createJsonQualifierImplementation())
assertThat(withArgs).isEqualTo(withArgs)
assertThat(withArgs).isEqualTo(TestQualifier::class.java.createJsonQualifierImplementation(mapOf("foo" to "bar")))
assertThat(withArgs).isNotEqualTo(TestQualifier::class.java.createJsonQualifierImplementation(mapOf("foo" to "baz")))
assertThat(noArgs).isNotEqualTo(withArgs)
assertThat(withArgs).isNotEqualTo(noArgs)
}

@Test
fun testCreateJsonQualifierImplementation_hashCode() {
assertThat(noArgs.hashCode()).isEqualTo(noArgs.hashCode())
assertThat(noArgs.hashCode()).isEqualTo(TestQualifier::class.java.createJsonQualifierImplementation().hashCode())
assertThat(withArgs.hashCode()).isEqualTo(withArgs.hashCode())
assertThat(withArgs.hashCode()).isEqualTo(TestQualifier::class.java.createJsonQualifierImplementation(mapOf("foo" to "bar")).hashCode())
assertThat(withArgs.hashCode()).isNotEqualTo(TestQualifier::class.java.createJsonQualifierImplementation(mapOf("foo" to "baz")).hashCode())
assertThat(noArgs.hashCode()).isNotEqualTo(withArgs.hashCode())
assertThat(withArgs.hashCode()).isNotEqualTo(noArgs.hashCode())
}

@Test
fun testCreateJsonQualifierImplementation_toString() {
assertThat(noArgs.toString()).isEqualTo("@se.ansman.kotshi.TestQualifier()")
assertThat(withArgs.toString()).isEqualTo("@se.ansman.kotshi.TestQualifier(foo=bar)")
}
}

@JsonQualifier
private annotation class TestQualifier(val foo: String = "n/a")

0 comments on commit 5baf65b

Please sign in to comment.