Promote Kotlin type-inferring APIs to the main Moshi package (round 2!) (#1202)

* Make moshi-root a kotlin project

* Move moshi kotlin extensions to moshi core

* Add appropriate experimental annotations

* Add nextAdapter helper

* Add explicit return type on addAdapter

* Expression body for adapter

* Use nextAdapter helper

* Opportunistically fix a couple Util warnings

* Add Types extensions

* Spotless

* Use extensions in more places for added coverage

* Apply java versions on any java plugin type

This way the kotlin projects get this too

* Fix circularAdapters test?

* Use java 8 in java for code gen too

* Fixup with CircularAdaptersTest

* Add coverage for remaining

* Remove nextAdapter

* Remove leftover function

* Use asserts

left checkNotNull for the contract

* boxIfPrimitive

* Fixup docs

* Copyright fixes

* Add parameterized addAdapter

* Switch to using native javaType API

* Spotless

* Back to 2019

* Spotless

* Use rawType extension

* Fix rebase issues
This commit is contained in:
Zac Sweers
2020-10-04 18:18:52 -04:00
committed by GitHub
parent f17e7c2584
commit 230c3d801f
17 changed files with 298 additions and 152 deletions

View File

@@ -120,6 +120,7 @@ subprojects {
} }
} }
// Apply with "java" instead of just "java-library" so kotlin projects get it too
pluginManager.withPlugin("java") { pluginManager.withPlugin("java") {
configure<JavaPluginExtension> { configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_7 sourceCompatibility = JavaVersion.VERSION_1_7

View File

@@ -36,6 +36,7 @@ object Dependencies {
object Kotlin { object Kotlin {
const val version = "1.4.10" const val version = "1.4.10"
const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib:$version"
const val metadata = "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.1.0" const val metadata = "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.1.0"
} }

View File

@@ -25,6 +25,11 @@ plugins {
id("com.github.johnrengelman.shadow") version "6.0.0" id("com.github.johnrengelman.shadow") version "6.0.0"
} }
configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType<KotlinCompile>().configureEach { tasks.withType<KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"

View File

@@ -21,10 +21,10 @@ import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.internal.Util import com.squareup.moshi.internal.Util
import com.squareup.moshi.internal.Util.generatedAdapter import com.squareup.moshi.internal.Util.generatedAdapter
import com.squareup.moshi.internal.Util.resolve import com.squareup.moshi.internal.Util.resolve
import com.squareup.moshi.rawType
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import java.lang.reflect.Type import java.lang.reflect.Type
import java.util.AbstractMap.SimpleEntry import java.util.AbstractMap.SimpleEntry
@@ -184,7 +184,7 @@ class KotlinJsonAdapterFactory : JsonAdapter.Factory {
JsonAdapter<*>? { JsonAdapter<*>? {
if (annotations.isNotEmpty()) return null if (annotations.isNotEmpty()) return null
val rawType = Types.getRawType(type) val rawType = type.rawType
if (rawType.isInterface) return null if (rawType.isInterface) return null
if (rawType.isEnum) return null if (rawType.isEnum) return null
if (!rawType.isAnnotationPresent(KOTLIN_METADATA)) return null if (!rawType.isAnnotationPresent(KOTLIN_METADATA)) return null

View File

@@ -17,6 +17,7 @@ package com.squareup.moshi.kotlin
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import org.junit.Test import org.junit.Test
class DefaultConstructorTest { class DefaultConstructorTest {
@@ -25,7 +26,7 @@ class DefaultConstructorTest {
val expected = TestClass("requiredClass") val expected = TestClass("requiredClass")
val json = val json =
"""{"required":"requiredClass"}""" """{"required":"requiredClass"}"""
val instance = Moshi.Builder().build().adapter<TestClass>(TestClass::class.java) val instance = Moshi.Builder().build().adapter<TestClass>()
.fromJson(json)!! .fromJson(json)!!
check(instance == expected) { check(instance == expected) {
"No match:\nActual : $instance\nExpected: $expected" "No match:\nActual : $instance\nExpected: $expected"
@@ -36,7 +37,7 @@ class DefaultConstructorTest {
val expected = TestClass("requiredClass", "customOptional", 4, "setDynamic", 5, 6) val expected = TestClass("requiredClass", "customOptional", 4, "setDynamic", 5, 6)
val json = val json =
"""{"required":"requiredClass","optional":"customOptional","optional2":4,"dynamicSelfReferenceOptional":"setDynamic","dynamicOptional":5,"dynamicInlineOptional":6}""" """{"required":"requiredClass","optional":"customOptional","optional2":4,"dynamicSelfReferenceOptional":"setDynamic","dynamicOptional":5,"dynamicInlineOptional":6}"""
val instance = Moshi.Builder().build().adapter<TestClass>(TestClass::class.java) val instance = Moshi.Builder().build().adapter<TestClass>()
.fromJson(json)!! .fromJson(json)!!
check(instance == expected) { check(instance == expected) {
"No match:\nActual : $instance\nExpected: $expected" "No match:\nActual : $instance\nExpected: $expected"
@@ -47,7 +48,7 @@ class DefaultConstructorTest {
val expected = TestClass("requiredClass", "customOptional") val expected = TestClass("requiredClass", "customOptional")
val json = val json =
"""{"required":"requiredClass","optional":"customOptional"}""" """{"required":"requiredClass","optional":"customOptional"}"""
val instance = Moshi.Builder().build().adapter<TestClass>(TestClass::class.java) val instance = Moshi.Builder().build().adapter<TestClass>()
.fromJson(json)!! .fromJson(json)!!
check(instance == expected) { check(instance == expected) {
"No match:\nActual : $instance\nExpected: $expected" "No match:\nActual : $instance\nExpected: $expected"

View File

@@ -24,9 +24,10 @@ import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson import com.squareup.moshi.ToJson
import com.squareup.moshi.Types import com.squareup.moshi.adapter
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.squareup.moshi.kotlin.reflect.adapter import com.squareup.moshi.rawType
import com.squareup.moshi.supertypeOf
import org.intellij.lang.annotations.Language import org.intellij.lang.annotations.Language
import org.junit.Assert.fail import org.junit.Assert.fail
import org.junit.Test import org.junit.Test
@@ -62,11 +63,11 @@ class DualKotlinTest(useReflection: Boolean) {
object : Factory { object : Factory {
override fun create( override fun create(
type: Type, type: Type,
annotations: MutableSet<out Annotation>, annotations: Set<Annotation>,
moshi: Moshi moshi: Moshi
): JsonAdapter<*>? { ): JsonAdapter<*>? {
// Prevent falling back to generated adapter lookup // Prevent falling back to generated adapter lookup
val rawType = Types.getRawType(type) val rawType = type.rawType
val metadataClass = Class.forName("kotlin.Metadata") as Class<out Annotation> val metadataClass = Class.forName("kotlin.Metadata") as Class<out Annotation>
check(rawType.isEnum || !rawType.isAnnotationPresent(metadataClass)) { check(rawType.isEnum || !rawType.isAnnotationPresent(metadataClass)) {
"Unhandled Kotlin type in reflective test! $rawType" "Unhandled Kotlin type in reflective test! $rawType"
@@ -446,7 +447,7 @@ class DualKotlinTest(useReflection: Boolean) {
@Test fun typeAliasUnwrapping() { @Test fun typeAliasUnwrapping() {
val adapter = moshi val adapter = moshi
.newBuilder() .newBuilder()
.add(Types.supertypeOf(Int::class.javaObjectType), moshi.adapter<Int>()) .add(supertypeOf<Int>(), moshi.adapter<Int>())
.build() .build()
.adapter<TypeAliasUnwrapping>() .adapter<TypeAliasUnwrapping>()

View File

@@ -20,7 +20,7 @@ package com.squareup.moshi.kotlin.codegen
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.adapter import com.squareup.moshi.adapter
import org.intellij.lang.annotations.Language import org.intellij.lang.annotations.Language
import org.junit.Test import org.junit.Test

View File

@@ -26,9 +26,8 @@ import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson import com.squareup.moshi.ToJson
import com.squareup.moshi.Types import com.squareup.moshi.adapter
import com.squareup.moshi.internal.NullSafeJsonAdapter import com.squareup.moshi.internal.NullSafeJsonAdapter
import com.squareup.moshi.kotlin.reflect.adapter
import org.intellij.lang.annotations.Language import org.intellij.lang.annotations.Language
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.fail import org.junit.Assert.fail
@@ -299,13 +298,7 @@ class GeneratedAdaptersTest {
@Test @Test
fun nullableTypeParams() { fun nullableTypeParams() {
val adapter = moshi.adapter<NullableTypeParams<Int>>( val adapter = moshi.adapter<NullableTypeParams<Int>>()
Types.newParameterizedTypeWithOwner(
GeneratedAdaptersTest::class.java,
NullableTypeParams::class.java,
Int::class.javaObjectType
)
)
val nullSerializing = adapter.serializeNulls() val nullSerializing = adapter.serializeNulls()
val nullableTypeParams = NullableTypeParams( val nullableTypeParams = NullableTypeParams(
@@ -597,7 +590,7 @@ class GeneratedAdaptersTest {
@Test fun multipleTransientConstructorParameters() { @Test fun multipleTransientConstructorParameters() {
val moshi = Moshi.Builder().build() val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(MultipleTransientConstructorParameters::class.java) val jsonAdapter = moshi.adapter<MultipleTransientConstructorParameters>()
val encoded = MultipleTransientConstructorParameters(3, 5, 7) val encoded = MultipleTransientConstructorParameters(3, 5, 7)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
@@ -1346,6 +1339,7 @@ class GeneratedAdaptersTest {
@Test fun typesSizeCheckMessages_noArgs() { @Test fun typesSizeCheckMessages_noArgs() {
try { try {
// Note: This is impossible to do if you use the reified adapter extension!
moshi.adapter(MultipleGenerics::class.java) moshi.adapter(MultipleGenerics::class.java)
fail("Should have failed to construct the adapter due to missing generics") fail("Should have failed to construct the adapter due to missing generics")
} catch (e: RuntimeException) { } catch (e: RuntimeException) {

View File

@@ -17,6 +17,7 @@ package com.squareup.moshi.kotlin.codegen
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import org.intellij.lang.annotations.Language import org.intellij.lang.annotations.Language
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
@@ -41,7 +42,7 @@ class MultipleMasksTest {
val json = val json =
"""{"arg50":500,"arg3":34,"arg11":11,"arg65":67}""" """{"arg50":500,"arg3":34,"arg11":11,"arg65":67}"""
val instance = Moshi.Builder().build().adapter(MultipleMasks::class.java) val instance = Moshi.Builder().build().adapter<MultipleMasks>()
.fromJson(json)!! .fromJson(json)!!
assertEquals(instance.arg2, 2) assertEquals(instance.arg2, 2)

View File

@@ -1,106 +0,0 @@
/*
* Copyright (C) 2019 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.kotlin.reflect
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.internal.NonNullJsonAdapter
import com.squareup.moshi.internal.NullSafeJsonAdapter
import java.lang.reflect.Type
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.KTypeParameter
import kotlin.reflect.KTypeProjection
import kotlin.reflect.KVariance
import kotlin.reflect.typeOf
/**
* @return a [JsonAdapter] for [T], creating it if necessary. Note that while nullability of [T]
* itself is handled, nested types (such as in generics) are not resolved.
*/
inline fun <reified T> Moshi.adapter(): JsonAdapter<T> {
return adapter(typeOf<T>())
}
inline fun <reified T> Moshi.Builder.addAdapter(adapter: JsonAdapter<T>) = add(typeOf<T>().toType(), adapter)
/**
* @return a [JsonAdapter] for [ktype], creating it if necessary. Note that while nullability of
* [ktype] itself is handled, nested types (such as in generics) are not resolved.
*/
fun <T> Moshi.adapter(ktype: KType): JsonAdapter<T> {
val adapter = adapter<T>(ktype.toType())
return if (adapter is NullSafeJsonAdapter || adapter is NonNullJsonAdapter) {
// TODO CR - Assume that these know what they're doing? Or should we defensively avoid wrapping for matching nullability?
adapter
} else if (ktype.isMarkedNullable) {
adapter.nullSafe()
} else {
adapter.nonNull()
}
}
@PublishedApi
internal fun KType.toType(allowPrimitives: Boolean = true): Type {
classifier?.let {
when (it) {
is KTypeParameter -> throw IllegalArgumentException("Type parameters are not supported")
is KClass<*> -> {
val javaType = if (allowPrimitives) {
it.java
} else {
it.javaObjectType
}
if (javaType.isArray) {
return Types.arrayOf(javaType.componentType)
}
return if (arguments.isEmpty()) {
javaType
} else {
val typeArguments = arguments.toTypedArray { it.toType() }
val enclosingClass = javaType.enclosingClass
return if (enclosingClass != null) {
Types.newParameterizedTypeWithOwner(enclosingClass, javaType, *typeArguments)
} else {
Types.newParameterizedType(javaType, *typeArguments)
}
}
}
else -> throw IllegalArgumentException("Unsupported classifier: $this")
}
}
// Can happen for intersection types
throw IllegalArgumentException("Unrepresentable type: $this")
}
internal fun KTypeProjection.toType(): Type {
val javaType = type?.toType(allowPrimitives = false) ?: return Any::class.java
return when (variance) {
null -> Any::class.java
KVariance.INVARIANT -> javaType
KVariance.IN -> Types.subtypeOf(javaType)
KVariance.OUT -> Types.supertypeOf(javaType)
}
}
private inline fun <T, reified R> List<T>.toTypedArray(mapper: (T) -> R): Array<R> {
return Array(size) {
mapper.invoke(get(it))
}
}

View File

@@ -26,7 +26,7 @@ import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson import com.squareup.moshi.ToJson
import com.squareup.moshi.Types import com.squareup.moshi.adapter
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions
import org.junit.Assert.fail import org.junit.Assert.fail
import org.junit.Test import org.junit.Test
@@ -448,7 +448,7 @@ class KotlinJsonAdapterTest {
fail() fail()
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
assertThat(e).hasMessageThat().isEqualTo( assertThat(e).hasMessageThat().isEqualTo(
"Platform class kotlin.Triple in kotlin.Triple<java.lang.Object, java.lang.Object, java.lang.Object> requires explicit JsonAdapter to be registered" "Platform class kotlin.Triple in kotlin.Triple<?, ?, ?> requires explicit JsonAdapter to be registered"
) )
} }
} }
@@ -857,13 +857,7 @@ class KotlinJsonAdapterTest {
@Test fun genericTypes() { @Test fun genericTypes() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val stringBoxAdapter = moshi.adapter<Box<String>>( val stringBoxAdapter = moshi.adapter<Box<String>>()
Types.newParameterizedTypeWithOwner(
KotlinJsonAdapterTest::class.java,
Box::class.java,
String::class.java
)
)
assertThat(stringBoxAdapter.fromJson("""{"data":"hello"}""")).isEqualTo(Box("hello")) assertThat(stringBoxAdapter.fromJson("""{"data":"hello"}""")).isEqualTo(Box("hello"))
assertThat(stringBoxAdapter.toJson(Box("hello"))).isEqualTo("""{"data":"hello"}""") assertThat(stringBoxAdapter.toJson(Box("hello"))).isEqualTo("""{"data":"hello"}""")
} }
@@ -872,18 +866,7 @@ class KotlinJsonAdapterTest {
@Test fun nestedGenericTypes() { @Test fun nestedGenericTypes() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val type = Types.newParameterizedTypeWithOwner( val adapter = moshi.adapter<NestedGenerics<String, Int, Box<String>>>().indent(" ")
KotlinJsonAdapterTest::class.java,
NestedGenerics::class.java,
String::class.java,
Int::class.javaObjectType,
Types.newParameterizedTypeWithOwner(
KotlinJsonAdapterTest::class.java,
Box::class.java,
String::class.java
)
)
val adapter = moshi.adapter<NestedGenerics<String, Int, Box<String>>>(type).indent(" ")
val json = val json =
""" """
|{ |{

View File

@@ -14,15 +14,30 @@
* limitations under the License. * limitations under the License.
*/ */
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
`java-library` kotlin("jvm")
id("com.vanniktech.maven.publish") id("com.vanniktech.maven.publish")
} }
tasks.withType<KotlinCompile>()
.matching { it.name.contains("test", true) }
.configureEach {
kotlinOptions {
@Suppress("SuspiciousCollectionReassignment") // It's not suspicious
freeCompilerArgs += listOf(
"-Xopt-in=kotlin.ExperimentalStdlibApi"
)
}
}
dependencies { dependencies {
compileOnly(Dependencies.jsr305) compileOnly(Dependencies.jsr305)
compileOnly(Dependencies.Kotlin.stdlib)
api(Dependencies.okio) api(Dependencies.okio)
testImplementation(Dependencies.Kotlin.stdlib)
testCompileOnly(Dependencies.jsr305) testCompileOnly(Dependencies.jsr305)
testImplementation(Dependencies.Testing.junit) testImplementation(Dependencies.Testing.junit)
testImplementation(Dependencies.Testing.truth) testImplementation(Dependencies.Testing.truth)

View File

@@ -18,3 +18,7 @@ POM_NAME=Moshi
POM_ARTIFACT_ID=moshi POM_ARTIFACT_ID=moshi
POM_PACKAGING=jar POM_PACKAGING=jar
AUTOMATIC_MODULE_NAME=com.squareup.moshi AUTOMATIC_MODULE_NAME=com.squareup.moshi
# Kotlin adds the stdlib dep by default in 1.4.0+, but we want to effectively make it compileOnly
# for our case to avoid imposing it on consumers.
kotlin.stdlib.default.dependency=false

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2019 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi
import com.squareup.moshi.internal.NonNullJsonAdapter
import com.squareup.moshi.internal.NullSafeJsonAdapter
import kotlin.reflect.KType
import kotlin.reflect.javaType
import kotlin.reflect.typeOf
/**
* @return a [JsonAdapter] for [T], creating it if necessary. Note that while nullability of [T]
* itself is handled, nested types (such as in generics) are not resolved.
*/
@ExperimentalStdlibApi
inline fun <reified T> Moshi.adapter(): JsonAdapter<T> = adapter(typeOf<T>())
@ExperimentalStdlibApi
inline fun <reified T> Moshi.Builder.addAdapter(adapter: JsonAdapter<T>): Moshi.Builder = add(typeOf<T>().javaType, adapter)
/**
* @return a [JsonAdapter] for [ktype], creating it if necessary. Note that while nullability of
* [ktype] itself is handled, nested types (such as in generics) are not resolved.
*/
@ExperimentalStdlibApi
fun <T> Moshi.adapter(ktype: KType): JsonAdapter<T> {
val adapter = adapter<T>(ktype.javaType)
return if (adapter is NullSafeJsonAdapter || adapter is NonNullJsonAdapter) {
// TODO CR - Assume that these know what they're doing? Or should we defensively avoid wrapping for matching nullability?
adapter
} else if (ktype.isMarkedNullable) {
adapter.nullSafe()
} else {
adapter.nonNull()
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2020 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi
import com.squareup.moshi.internal.Util
import java.lang.reflect.GenericArrayType
import java.lang.reflect.Type
import java.lang.reflect.WildcardType
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.javaType
import kotlin.reflect.typeOf
/** Returns the raw [Class] type of this type. */
val Type.rawType: Class<*> get() = Types.getRawType(this)
/**
* Checks if [this] contains [T]. Returns the subset of [this] without [T], or null if
* [this] does not contain [T].
*/
inline fun <reified T : Annotation> Set<Annotation>.nextAnnotations(): Set<Annotation>? = Types.nextAnnotations(this, T::class.java)
/**
* Returns a type that represents an unknown type that extends [T]. For example, if
* [T] is [CharSequence], this returns `out CharSequence`. If
* [T] is [Any], this returns `*`, which is shorthand for `out Any?`.
*/
@ExperimentalStdlibApi
inline fun <reified T> subtypeOf(): WildcardType {
var type = typeOf<T>().javaType
if (type is Class<*>) {
type = Util.boxIfPrimitive(type)
}
return Types.subtypeOf(type)
}
/**
* Returns a type that represents an unknown supertype of [T] bound. For example, if [T] is
* [String], this returns `in String`.
*/
@ExperimentalStdlibApi
inline fun <reified T> supertypeOf(): WildcardType {
var type = typeOf<T>().javaType
if (type is Class<*>) {
type = Util.boxIfPrimitive(type)
}
return Types.supertypeOf(type)
}
/** Returns a [GenericArrayType] with [this] as its [GenericArrayType.getGenericComponentType]. */
@ExperimentalStdlibApi
fun KType.asArrayType(): GenericArrayType = javaType.asArrayType()
/** Returns a [GenericArrayType] with [this] as its [GenericArrayType.getGenericComponentType]. */
fun KClass<*>.asArrayType(): GenericArrayType = java.asArrayType()
/** Returns a [GenericArrayType] with [this] as its [GenericArrayType.getGenericComponentType]. */
fun Type.asArrayType(): GenericArrayType = Types.arrayOf(this)

View File

@@ -39,7 +39,9 @@ import java.lang.reflect.WildcardType;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Map;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -50,6 +52,9 @@ public final class Util {
@Nullable public static final Class<?> DEFAULT_CONSTRUCTOR_MARKER; @Nullable public static final Class<?> DEFAULT_CONSTRUCTOR_MARKER;
@Nullable private static final Class<? extends Annotation> METADATA; @Nullable private static final Class<? extends Annotation> METADATA;
/** A map from primitive types to their corresponding wrapper types. */
private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER_TYPE;
static { static {
Class<? extends Annotation> metadata = null; Class<? extends Annotation> metadata = null;
try { try {
@@ -67,6 +72,20 @@ public final class Util {
} catch (ClassNotFoundException ignored) { } catch (ClassNotFoundException ignored) {
} }
DEFAULT_CONSTRUCTOR_MARKER = defaultConstructorMarker; DEFAULT_CONSTRUCTOR_MARKER = defaultConstructorMarker;
Map<Class<?>, Class<?>> primToWrap = new LinkedHashMap<>(16);
primToWrap.put(boolean.class, Boolean.class);
primToWrap.put(byte.class, Byte.class);
primToWrap.put(char.class, Character.class);
primToWrap.put(double.class, Double.class);
primToWrap.put(float.class, Float.class);
primToWrap.put(int.class, Integer.class);
primToWrap.put(long.class, Long.class);
primToWrap.put(short.class, Short.class);
primToWrap.put(void.class, Void.class);
PRIMITIVE_TO_WRAPPER_TYPE = Collections.unmodifiableMap(primToWrap);
} }
// Extracted as a method with a keep rule to prevent R8 from keeping Kotlin Metada // Extracted as a method with a keep rule to prevent R8 from keeping Kotlin Metada
@@ -182,14 +201,14 @@ public final class Util {
} }
public static Type resolve(Type context, Class<?> contextRawType, Type toResolve) { public static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
return resolve(context, contextRawType, toResolve, new LinkedHashSet<TypeVariable>()); return resolve(context, contextRawType, toResolve, new LinkedHashSet<TypeVariable<?>>());
} }
private static Type resolve( private static Type resolve(
Type context, Type context,
Class<?> contextRawType, Class<?> contextRawType,
Type toResolve, Type toResolve,
Collection<TypeVariable> visitedTypeVariables) { Collection<TypeVariable<?>> visitedTypeVariables) {
// This implementation is made a little more complicated in an attempt to avoid object-creation. // This implementation is made a little more complicated in an attempt to avoid object-creation.
while (true) { while (true) {
if (toResolve instanceof TypeVariable) { if (toResolve instanceof TypeVariable) {
@@ -643,4 +662,12 @@ public final class Util {
} }
return new JsonDataException(message); return new JsonDataException(message);
} }
// Public due to inline access in MoshiKotlinTypesExtensions
public static <T> Class<T> boxIfPrimitive(Class<T> type) {
// cast is safe: long.class and Long.class are both of type Class<Long>
@SuppressWarnings("unchecked")
Class<T> wrapped = (Class<T>) PRIMITIVE_TO_WRAPPER_TYPE.get(type);
return (wrapped == null) ? type : wrapped;
}
} }

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2020 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.reflect.typeOf
@JsonQualifier
@Retention(RUNTIME)
annotation class TestAnnotation1
@JsonQualifier
@Retention(RUNTIME)
annotation class TestAnnotation2
@TestAnnotation1
@TestAnnotation2
class KotlinExtensionsTest {
@Test
fun nextAnnotationsShouldWork() {
val annotations = KotlinExtensionsTest::class.java.annotations
.filterTo(mutableSetOf()) {
it.annotationClass.java.isAnnotationPresent(JsonQualifier::class.java)
}
assertEquals(2, annotations.size)
val next = annotations.nextAnnotations<TestAnnotation2>()
checkNotNull(next)
assertEquals(1, next.size)
assertTrue(next.first() is TestAnnotation1)
}
@Test
fun arrayType() {
val stringArray = String::class.asArrayType()
check(stringArray.genericComponentType == String::class.java)
val stringListType = typeOf<List<String>>()
val stringListArray = stringListType.asArrayType()
val expected = Types.arrayOf(Types.newParameterizedType(List::class.java, String::class.java))
assertEquals(stringListArray, expected)
}
@Test
fun addAdapterInferred() {
// An adapter that always returns -1
val customIntdapter = object : JsonAdapter<Int>() {
override fun fromJson(reader: JsonReader): Int? {
reader.skipValue()
return -1
}
override fun toJson(writer: JsonWriter, value: Int?) {
throw NotImplementedError()
}
}
val moshi = Moshi.Builder()
.addAdapter(customIntdapter)
.build()
assertEquals(-1, moshi.adapter<Int>().fromJson("5"))
}
@Test
fun addAdapterInferred_parameterized() {
// An adapter that always returns listOf(-1)
val customIntListAdapter = object : JsonAdapter<List<Int>>() {
override fun fromJson(reader: JsonReader): List<Int>? {
reader.skipValue()
return listOf(-1)
}
override fun toJson(writer: JsonWriter, value: List<Int>?) {
throw NotImplementedError()
}
}
val moshi = Moshi.Builder()
.addAdapter(customIntListAdapter)
.build()
assertEquals(listOf(-1), moshi.adapter<List<Int>>().fromJson("[5]"))
}
}