Support covariant Map values, which are used by Kotlin

This commit is contained in:
Sye van der Veen 2019-04-30 11:12:04 -04:00
parent 0943ef5a61
commit 7e417840e2
5 changed files with 187 additions and 10 deletions

View file

@ -30,6 +30,8 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert.fail
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.lang.reflect.ParameterizedType
import java.lang.reflect.WildcardType
import java.util.Locale
import java.util.SimpleTimeZone
import kotlin.annotation.AnnotationRetention.RUNTIME
@ -982,4 +984,144 @@ class KotlinJsonAdapterTest {
}
class PlainKotlinClass
@Test fun mapOfStringToStandardReflectionWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToStandardReflection::class.java,
"""{"map":{"key":"value"}}""",
MapOfStringToStandardReflection(mapOf("key" to "value")))
}
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToStandardReflection(val map: Map<String, String> = mapOf())
@Test fun mapOfStringToStandardCodegenWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToStandardCodegen::class.java,
"""{"map":{"key":"value"}}""",
MapOfStringToStandardCodegen(mapOf("key" to "value")))
}
@JsonClass(generateAdapter = true)
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToStandardCodegen(val map: Map<String, String> = mapOf())
@Test fun mapOfStringToEnumReflectionWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToEnumReflection::class.java,
"""{"map":{"key":"A"}}""",
MapOfStringToEnumReflection(mapOf("key" to KotlinEnum.A)))
}
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToEnumReflection(val map: Map<String, KotlinEnum> = mapOf())
@Test fun mapOfStringToEnumCodegenWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToEnumCodegen::class.java,
"""{"map":{"key":"A"}}""",
MapOfStringToEnumCodegen(mapOf("key" to KotlinEnum.A)))
}
@JsonClass(generateAdapter = true)
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToEnumCodegen(val map: Map<String, KotlinEnum> = mapOf())
@Test fun mapOfStringToCollectionReflectionWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToCollectionReflection::class.java,
"""{"map":{"key":[]}}""",
MapOfStringToCollectionReflection(mapOf("key" to listOf())))
}
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToCollectionReflection(val map: Map<String, List<Int>> = mapOf())
@Test fun mapOfStringToCollectionCodegenWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToCollectionCodegen::class.java,
"""{"map":{"key":[]}}""",
MapOfStringToCollectionCodegen(mapOf("key" to listOf())))
}
@JsonClass(generateAdapter = true)
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToCollectionCodegen(val map: Map<String, List<Int>> = mapOf())
@Test fun mapOfStringToMapReflectionWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToMapReflection::class.java,
"""{"map":{"key":{}}}""",
MapOfStringToMapReflection(mapOf("key" to mapOf())))
}
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToMapReflection(val map: Map<String, Map<String, Int>> = mapOf())
@Test fun mapOfStringToMapCodegenWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToMapCodegen::class.java,
"""{"map":{"key":{}}}""",
MapOfStringToMapCodegen(mapOf("key" to mapOf())))
}
@JsonClass(generateAdapter = true)
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToMapCodegen(val map: Map<String, Map<String, Int>> = mapOf())
@Test fun mapOfStringToArrayReflectionWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToArrayReflection::class.java,
"""{"map":{"key":[]}}""",
MapOfStringToArrayReflection(mapOf("key" to arrayOf())))
}
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToArrayReflection(val map: Map<String, Array<Int>> = mapOf())
@Test fun mapOfStringToArrayCodegenWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToArrayCodegen::class.java,
"""{"map":{"key":[]}}""",
MapOfStringToArrayCodegen(mapOf("key" to arrayOf())))
}
@JsonClass(generateAdapter = true)
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToArrayCodegen(val map: Map<String, Array<Int>> = mapOf())
@Test fun mapOfStringToClassReflectionWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToClassReflection::class.java,
"""{"map":{"key":{"a":19,"b":42}}}""",
MapOfStringToClassReflection(mapOf("key" to ConstructorParameters(19, 42))))
}
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToClassReflection(val map: Map<String, ConstructorParameters> = mapOf())
@Test fun mapOfStringToClassCodegenWildcards() {
mapWildcardsParameterizedTest(
MapOfStringToClassCodegen::class.java,
"""{"map":{"key":{"a":19,"b":42}}}""",
MapOfStringToClassCodegen(mapOf("key" to ConstructorParameters(19, 42))))
}
@JsonClass(generateAdapter = true)
@JvmSuppressWildcards(suppress = false)
data class MapOfStringToClassCodegen(val map: Map<String, ConstructorParameters> = mapOf())
private fun <T> mapWildcardsParameterizedTest(type: Class<T>, json: String, value: T) {
// Ensure the map was created with the expected wildcards of a Kotlin map.
val fieldType = type.getDeclaredField("map").genericType
val fieldTypeArguments = (fieldType as ParameterizedType).actualTypeArguments
assertThat(fieldTypeArguments[0]).isNotInstanceOf(WildcardType::class.java)
assertThat(fieldTypeArguments[1]).isInstanceOf(WildcardType::class.java)
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val adapter = moshi.adapter(type)
assertThat(adapter.fromJson(json)).isEqualToComparingFieldByFieldRecursively(value)
assertThat(adapter.toJson(value)).isEqualTo(json)
}
}

View file

@ -34,6 +34,7 @@ import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.canonicalize;
import static com.squareup.moshi.internal.Util.removeSubtypeWildcard;
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
/**
@ -112,7 +113,7 @@ public final class Moshi {
throw new NullPointerException("annotations == null");
}
type = canonicalize(type);
type = removeSubtypeWildcard(canonicalize(type));
// If there's an equivalent adapter in the cache, we're done!
Object cacheKey = cacheKey(type, annotations);
@ -158,7 +159,7 @@ public final class Moshi {
Set<? extends Annotation> annotations) {
if (annotations == null) throw new NullPointerException("annotations == null");
type = canonicalize(type);
type = removeSubtypeWildcard(canonicalize(type));
int skipPastIndex = factories.indexOf(skipPast);
if (skipPastIndex == -1) {

View file

@ -139,6 +139,21 @@ public final class Util {
}
}
/**
* If type is a "? extends X" wildcard, returns X; otherwise returns type unchanged.
*/
public static Type removeSubtypeWildcard(Type type) {
if (!(type instanceof WildcardType)) return type;
Type[] lowerBounds = ((WildcardType) type).getLowerBounds();
if (lowerBounds.length != 0) return type;
Type[] upperBounds = ((WildcardType) type).getUpperBounds();
if (upperBounds.length != 1) throw new IllegalArgumentException();
return upperBounds[0];
}
public static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
// This implementation is made a little more complicated in an attempt to avoid object-creation.
while (true) {

View file

@ -86,6 +86,27 @@ public final class MapJsonAdapterTest {
assertThat(jsonAdapter.fromJson(jsonReader)).isEqualTo(null);
}
@Test public void covariantValue() throws Exception {
// Important for Kotlin maps, which are all Map<K, ? extends T>.
JsonAdapter<Map<String, Object>> jsonAdapter =
mapAdapter(String.class, Types.subtypeOf(Object.class));
Map<String, Object> map = new LinkedHashMap<>();
map.put("boolean", true);
map.put("float", 42.0);
map.put("String", "value");
String asJson = "{\"boolean\":true,\"float\":42.0,\"String\":\"value\"}";
Buffer buffer = new Buffer();
JsonWriter jsonWriter = JsonWriter.of(buffer);
jsonAdapter.toJson(jsonWriter, map);
assertThat(buffer.readUtf8()).isEqualTo(asJson);
JsonReader jsonReader = newReader(asJson);
assertThat(jsonAdapter.fromJson(jsonReader)).isEqualTo(map);
}
@Test public void orderIsRetained() throws Exception {
Map<String, Integer> map = new LinkedHashMap<>();
map.put("c", 1);

View file

@ -537,15 +537,13 @@ public final class MoshiTest {
assertThat(adapter.toJson(null)).isEqualTo("null");
}
@Test public void upperBoundedWildcardsAreNotHandled() {
@Test public void upperBoundedWildcardsAreHandled() throws Exception {
Moshi moshi = new Moshi.Builder().build();
try {
moshi.adapter(Types.subtypeOf(String.class));
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage(
"No JsonAdapter for ? extends java.lang.String (with no annotations)");
}
JsonAdapter<Object> adapter = moshi.adapter(Types.subtypeOf(String.class));
assertThat(adapter.fromJson("\"a\"")).isEqualTo("a");
assertThat(adapter.toJson("b")).isEqualTo("\"b\"");
assertThat(adapter.fromJson("null")).isEqualTo(null);
assertThat(adapter.toJson(null)).isEqualTo("null");
}
@Test public void lowerBoundedWildcardsAreNotHandled() {