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

@@ -14,15 +14,30 @@
* limitations under the License.
*/
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
`java-library`
kotlin("jvm")
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 {
compileOnly(Dependencies.jsr305)
compileOnly(Dependencies.Kotlin.stdlib)
api(Dependencies.okio)
testImplementation(Dependencies.Kotlin.stdlib)
testCompileOnly(Dependencies.jsr305)
testImplementation(Dependencies.Testing.junit)
testImplementation(Dependencies.Testing.truth)

View File

@@ -18,3 +18,7 @@ POM_NAME=Moshi
POM_ARTIFACT_ID=moshi
POM_PACKAGING=jar
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.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nullable;
@@ -50,6 +52,9 @@ public final class Util {
@Nullable public static final Class<?> DEFAULT_CONSTRUCTOR_MARKER;
@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 {
Class<? extends Annotation> metadata = null;
try {
@@ -67,6 +72,20 @@ public final class Util {
} catch (ClassNotFoundException ignored) {
}
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
@@ -182,14 +201,14 @@ public final class Util {
}
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(
Type context,
Class<?> contextRawType,
Type toResolve,
Collection<TypeVariable> visitedTypeVariables) {
Collection<TypeVariable<?>> visitedTypeVariables) {
// This implementation is made a little more complicated in an attempt to avoid object-creation.
while (true) {
if (toResolve instanceof TypeVariable) {
@@ -643,4 +662,12 @@ public final class Util {
}
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]"))
}
}