Merge pull request #821 from Syeberman/sye/covariant-map-values
Support covariant Map values, which are used by Kotlin
This commit is contained in:
commit
f1965f0f46
5 changed files with 187 additions and 10 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in a new issue