Convert StandardJsonAdapters to Kotlin (#1494)

* Rename .java to .kt

* Convert StandardJsonAdapters to Kotlin

* Inline factory to object

* Idiomatic cleanups + japicmp

* Fixes

* Another idiomatatic bit

* Use knownNotNull() where applicable
This commit is contained in:
Zac Sweers
2022-01-10 12:14:07 -05:00
committed by GitHub
parent 323d97c787
commit 6e81499501
5 changed files with 282 additions and 399 deletions

View File

@@ -28,7 +28,8 @@ val japicmp = tasks.register<JapicmpTask>("japicmp") {
classExcludes = listOf(
"com.squareup.moshi.internal.NonNullJsonAdapter", // Internal.
"com.squareup.moshi.internal.NullSafeJsonAdapter", // Internal.
"com.squareup.moshi.internal.Util" // Internal.
"com.squareup.moshi.internal.Util", // Internal.
"com.squareup.moshi.StandardJsonAdapters" // Package-private
)
methodExcludes = listOf(
"com.squareup.moshi.JsonAdapter#indent(java.lang.String)" // Was unintentionally open before

View File

@@ -329,7 +329,7 @@ public class Moshi internal constructor(builder: Builder) {
internal companion object {
@JvmField
val BUILT_IN_FACTORIES: List<JsonAdapter.Factory> = buildList(6) {
add(StandardJsonAdapters.FACTORY)
add(StandardJsonAdapters)
add(CollectionJsonAdapter.Factory)
add(MapJsonAdapter.FACTORY)
add(ArrayJsonAdapter.FACTORY)

View File

@@ -1,396 +0,0 @@
/*
* Copyright (C) 2014 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 static com.squareup.moshi.internal.Util.generatedAdapter;
import com.squareup.moshi.internal.Util;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
final class StandardJsonAdapters {
private StandardJsonAdapters() {}
public static final JsonAdapter.Factory FACTORY =
new JsonAdapter.Factory() {
@Override
public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!annotations.isEmpty()) return null;
if (type == boolean.class) return BOOLEAN_JSON_ADAPTER;
if (type == byte.class) return BYTE_JSON_ADAPTER;
if (type == char.class) return CHARACTER_JSON_ADAPTER;
if (type == double.class) return DOUBLE_JSON_ADAPTER;
if (type == float.class) return FLOAT_JSON_ADAPTER;
if (type == int.class) return INTEGER_JSON_ADAPTER;
if (type == long.class) return LONG_JSON_ADAPTER;
if (type == short.class) return SHORT_JSON_ADAPTER;
if (type == Boolean.class) return BOOLEAN_JSON_ADAPTER.nullSafe();
if (type == Byte.class) return BYTE_JSON_ADAPTER.nullSafe();
if (type == Character.class) return CHARACTER_JSON_ADAPTER.nullSafe();
if (type == Double.class) return DOUBLE_JSON_ADAPTER.nullSafe();
if (type == Float.class) return FLOAT_JSON_ADAPTER.nullSafe();
if (type == Integer.class) return INTEGER_JSON_ADAPTER.nullSafe();
if (type == Long.class) return LONG_JSON_ADAPTER.nullSafe();
if (type == Short.class) return SHORT_JSON_ADAPTER.nullSafe();
if (type == String.class) return STRING_JSON_ADAPTER.nullSafe();
if (type == Object.class) return new ObjectJsonAdapter(moshi).nullSafe();
Class<?> rawType = Types.getRawType(type);
@Nullable JsonAdapter<?> generatedAdapter = generatedAdapter(moshi, type, rawType);
if (generatedAdapter != null) {
return generatedAdapter;
}
if (rawType.isEnum()) {
//noinspection unchecked
return new EnumJsonAdapter<>((Class<? extends Enum>) rawType).nullSafe();
}
return null;
}
};
private static final String ERROR_FORMAT = "Expected %s but was %s at path %s";
static int rangeCheckNextInt(JsonReader reader, String typeMessage, int min, int max)
throws IOException {
int value = reader.nextInt();
if (value < min || value > max) {
throw new JsonDataException(
String.format(ERROR_FORMAT, typeMessage, value, reader.getPath()));
}
return value;
}
static final JsonAdapter<Boolean> BOOLEAN_JSON_ADAPTER =
new JsonAdapter<Boolean>() {
@Override
public Boolean fromJson(JsonReader reader) throws IOException {
return reader.nextBoolean();
}
@Override
public void toJson(JsonWriter writer, Boolean value) throws IOException {
writer.value(value.booleanValue());
}
@Override
public String toString() {
return "JsonAdapter(Boolean)";
}
};
static final JsonAdapter<Byte> BYTE_JSON_ADAPTER =
new JsonAdapter<Byte>() {
@Override
public Byte fromJson(JsonReader reader) throws IOException {
return (byte) rangeCheckNextInt(reader, "a byte", Byte.MIN_VALUE, 0xff);
}
@Override
public void toJson(JsonWriter writer, Byte value) throws IOException {
writer.value(value.intValue() & 0xff);
}
@Override
public String toString() {
return "JsonAdapter(Byte)";
}
};
static final JsonAdapter<Character> CHARACTER_JSON_ADAPTER =
new JsonAdapter<Character>() {
@Override
public Character fromJson(JsonReader reader) throws IOException {
String value = reader.nextString();
if (value.length() > 1) {
throw new JsonDataException(
String.format(ERROR_FORMAT, "a char", '"' + value + '"', reader.getPath()));
}
return value.charAt(0);
}
@Override
public void toJson(JsonWriter writer, Character value) throws IOException {
writer.value(value.toString());
}
@Override
public String toString() {
return "JsonAdapter(Character)";
}
};
static final JsonAdapter<Double> DOUBLE_JSON_ADAPTER =
new JsonAdapter<Double>() {
@Override
public Double fromJson(JsonReader reader) throws IOException {
return reader.nextDouble();
}
@Override
public void toJson(JsonWriter writer, Double value) throws IOException {
writer.value(value.doubleValue());
}
@Override
public String toString() {
return "JsonAdapter(Double)";
}
};
static final JsonAdapter<Float> FLOAT_JSON_ADAPTER =
new JsonAdapter<Float>() {
@Override
public Float fromJson(JsonReader reader) throws IOException {
float value = (float) reader.nextDouble();
// Double check for infinity after float conversion; many doubles > Float.MAX
if (!reader.isLenient() && Float.isInfinite(value)) {
throw new JsonDataException(
"JSON forbids NaN and infinities: " + value + " at path " + reader.getPath());
}
return value;
}
@Override
public void toJson(JsonWriter writer, Float value) throws IOException {
// Manual null pointer check for the float.class adapter.
if (value == null) {
throw new NullPointerException();
}
// Use the Number overload so we write out float precision instead of double precision.
writer.value(value);
}
@Override
public String toString() {
return "JsonAdapter(Float)";
}
};
static final JsonAdapter<Integer> INTEGER_JSON_ADAPTER =
new JsonAdapter<Integer>() {
@Override
public Integer fromJson(JsonReader reader) throws IOException {
return reader.nextInt();
}
@Override
public void toJson(JsonWriter writer, Integer value) throws IOException {
writer.value(value.intValue());
}
@Override
public String toString() {
return "JsonAdapter(Integer)";
}
};
static final JsonAdapter<Long> LONG_JSON_ADAPTER =
new JsonAdapter<Long>() {
@Override
public Long fromJson(JsonReader reader) throws IOException {
return reader.nextLong();
}
@Override
public void toJson(JsonWriter writer, Long value) throws IOException {
writer.value(value.longValue());
}
@Override
public String toString() {
return "JsonAdapter(Long)";
}
};
static final JsonAdapter<Short> SHORT_JSON_ADAPTER =
new JsonAdapter<Short>() {
@Override
public Short fromJson(JsonReader reader) throws IOException {
return (short) rangeCheckNextInt(reader, "a short", Short.MIN_VALUE, Short.MAX_VALUE);
}
@Override
public void toJson(JsonWriter writer, Short value) throws IOException {
writer.value(value.intValue());
}
@Override
public String toString() {
return "JsonAdapter(Short)";
}
};
static final JsonAdapter<String> STRING_JSON_ADAPTER =
new JsonAdapter<String>() {
@Override
public String fromJson(JsonReader reader) throws IOException {
return reader.nextString();
}
@Override
public void toJson(JsonWriter writer, String value) throws IOException {
writer.value(value);
}
@Override
public String toString() {
return "JsonAdapter(String)";
}
};
static final class EnumJsonAdapter<T extends Enum<T>> extends JsonAdapter<T> {
private final Class<T> enumType;
private final String[] nameStrings;
private final T[] constants;
private final JsonReader.Options options;
EnumJsonAdapter(Class<T> enumType) {
this.enumType = enumType;
try {
constants = enumType.getEnumConstants();
nameStrings = new String[constants.length];
for (int i = 0; i < constants.length; i++) {
T constant = constants[i];
String constantName = constant.name();
nameStrings[i] = Util.jsonName(enumType.getField(constantName), constantName);
}
options = JsonReader.Options.of(nameStrings);
} catch (NoSuchFieldException e) {
throw new AssertionError("Missing field in " + enumType.getName(), e);
}
}
@Override
public T fromJson(JsonReader reader) throws IOException {
int index = reader.selectString(options);
if (index != -1) return constants[index];
// We can consume the string safely, we are terminating anyway.
String path = reader.getPath();
String name = reader.nextString();
throw new JsonDataException(
"Expected one of "
+ Arrays.asList(nameStrings)
+ " but was "
+ name
+ " at path "
+ path);
}
@Override
public void toJson(JsonWriter writer, T value) throws IOException {
writer.value(nameStrings[value.ordinal()]);
}
@Override
public String toString() {
return "JsonAdapter(" + enumType.getName() + ")";
}
}
/**
* This adapter is used when the declared type is {@code java.lang.Object}. Typically the runtime
* type is something else, and when encoding JSON this delegates to the runtime type's adapter.
* For decoding (where there is no runtime type to inspect), this uses maps and lists.
*
* <p>This adapter needs a Moshi instance to look up the appropriate adapter for runtime types as
* they are encountered.
*/
static final class ObjectJsonAdapter extends JsonAdapter<Object> {
private final Moshi moshi;
private final JsonAdapter<List> listJsonAdapter;
private final JsonAdapter<Map> mapAdapter;
private final JsonAdapter<String> stringAdapter;
private final JsonAdapter<Double> doubleAdapter;
private final JsonAdapter<Boolean> booleanAdapter;
ObjectJsonAdapter(Moshi moshi) {
this.moshi = moshi;
this.listJsonAdapter = moshi.adapter(List.class);
this.mapAdapter = moshi.adapter(Map.class);
this.stringAdapter = moshi.adapter(String.class);
this.doubleAdapter = moshi.adapter(Double.class);
this.booleanAdapter = moshi.adapter(Boolean.class);
}
@Override
public Object fromJson(JsonReader reader) throws IOException {
switch (reader.peek()) {
case BEGIN_ARRAY:
return listJsonAdapter.fromJson(reader);
case BEGIN_OBJECT:
return mapAdapter.fromJson(reader);
case STRING:
return stringAdapter.fromJson(reader);
case NUMBER:
return doubleAdapter.fromJson(reader);
case BOOLEAN:
return booleanAdapter.fromJson(reader);
case NULL:
return reader.nextNull();
default:
throw new IllegalStateException(
"Expected a value but was " + reader.peek() + " at path " + reader.getPath());
}
}
@Override
public void toJson(JsonWriter writer, Object value) throws IOException {
Class<?> valueClass = value.getClass();
if (valueClass == Object.class) {
// Don't recurse infinitely when the runtime type is also Object.class.
writer.beginObject();
writer.endObject();
} else {
moshi.adapter(toJsonType(valueClass), Util.NO_ANNOTATIONS).toJson(writer, value);
}
}
/**
* Returns the type to look up a type adapter for when writing {@code value} to JSON. Without
* this, attempts to emit standard types like `LinkedHashMap` would fail because Moshi doesn't
* provide built-in adapters for implementation types. It knows how to <strong>write</strong>
* those types, but lacks a mechanism to read them because it doesn't know how to find the
* appropriate constructor.
*/
private Class<?> toJsonType(Class<?> valueClass) {
if (Map.class.isAssignableFrom(valueClass)) return Map.class;
if (Collection.class.isAssignableFrom(valueClass)) return Collection.class;
return valueClass;
}
@Override
public String toString() {
return "JsonAdapter(Object)";
}
}
}

View File

@@ -0,0 +1,275 @@
/*
* Copyright (C) 2014 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.NO_ANNOTATIONS
import com.squareup.moshi.internal.generatedAdapter
import com.squareup.moshi.internal.jsonName
import com.squareup.moshi.internal.knownNotNull
import java.lang.reflect.Type
internal object StandardJsonAdapters : JsonAdapter.Factory {
private const val ERROR_FORMAT = "Expected %s but was %s at path %s"
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
if (annotations.isNotEmpty()) return null
if (type === Boolean::class.javaPrimitiveType) return BOOLEAN_JSON_ADAPTER
if (type === Byte::class.javaPrimitiveType) return BYTE_JSON_ADAPTER
if (type === Char::class.javaPrimitiveType) return CHARACTER_JSON_ADAPTER
if (type === Double::class.javaPrimitiveType) return DOUBLE_JSON_ADAPTER
if (type === Float::class.javaPrimitiveType) return FLOAT_JSON_ADAPTER
if (type === Int::class.javaPrimitiveType) return INTEGER_JSON_ADAPTER
if (type === Long::class.javaPrimitiveType) return LONG_JSON_ADAPTER
if (type === Short::class.javaPrimitiveType) return SHORT_JSON_ADAPTER
if (type === Boolean::class.javaObjectType) return BOOLEAN_JSON_ADAPTER.nullSafe()
if (type === Byte::class.javaObjectType) return BYTE_JSON_ADAPTER.nullSafe()
if (type === Char::class.javaObjectType) return CHARACTER_JSON_ADAPTER.nullSafe()
if (type === Double::class.javaObjectType) return DOUBLE_JSON_ADAPTER.nullSafe()
if (type === Float::class.javaObjectType) return FLOAT_JSON_ADAPTER.nullSafe()
if (type === Int::class.javaObjectType) return INTEGER_JSON_ADAPTER.nullSafe()
if (type === Long::class.javaObjectType) return LONG_JSON_ADAPTER.nullSafe()
if (type === Short::class.javaObjectType) return SHORT_JSON_ADAPTER.nullSafe()
if (type === String::class.java) return STRING_JSON_ADAPTER.nullSafe()
if (type === Any::class.java) return ObjectJsonAdapter(moshi).nullSafe()
val rawType = type.rawType
val generatedAdapter = moshi.generatedAdapter(type, rawType)
if (generatedAdapter != null) {
return generatedAdapter
}
return if (rawType.isEnum) {
@Suppress("UNCHECKED_CAST")
EnumJsonAdapter(rawType as Class<out Enum<*>>).nullSafe()
} else null
}
fun rangeCheckNextInt(reader: JsonReader, typeMessage: String?, min: Int, max: Int): Int {
val value = reader.nextInt()
if (value !in min..max) {
throw JsonDataException(ERROR_FORMAT.format(typeMessage, value, reader.path))
}
return value
}
val BOOLEAN_JSON_ADAPTER: JsonAdapter<Boolean> = object : JsonAdapter<Boolean>() {
override fun fromJson(reader: JsonReader) = reader.nextBoolean()
override fun toJson(writer: JsonWriter, value: Boolean?) {
writer.value(knownNotNull(value))
}
override fun toString() = "JsonAdapter(Boolean)"
}
private val BYTE_JSON_ADAPTER: JsonAdapter<Byte> = object : JsonAdapter<Byte>() {
override fun fromJson(reader: JsonReader): Byte {
return rangeCheckNextInt(reader, "a byte", Byte.MIN_VALUE.toInt(), 0xff).toByte()
}
override fun toJson(writer: JsonWriter, value: Byte?) {
writer.value((knownNotNull(value).toInt() and 0xff).toLong())
}
override fun toString() = "JsonAdapter(Byte)"
}
private val CHARACTER_JSON_ADAPTER: JsonAdapter<Char> = object : JsonAdapter<Char>() {
override fun fromJson(reader: JsonReader): Char {
val value = reader.nextString()
if (value.length > 1) {
throw JsonDataException(ERROR_FORMAT.format("a char", "\"$value\"", reader.path))
}
return value[0]
}
override fun toJson(writer: JsonWriter, value: Char?) {
writer.value(knownNotNull(value).toString())
}
override fun toString() = "JsonAdapter(Character)"
}
private val DOUBLE_JSON_ADAPTER: JsonAdapter<Double> = object : JsonAdapter<Double>() {
override fun fromJson(reader: JsonReader): Double {
return reader.nextDouble()
}
override fun toJson(writer: JsonWriter, value: Double?) {
writer.value(knownNotNull(value).toDouble())
}
override fun toString() = "JsonAdapter(Double)"
}
private val FLOAT_JSON_ADAPTER: JsonAdapter<Float> = object : JsonAdapter<Float>() {
override fun fromJson(reader: JsonReader): Float {
val value = reader.nextDouble().toFloat()
// Double check for infinity after float conversion; many doubles > Float.MAX
if (!reader.isLenient && value.isInfinite()) {
throw JsonDataException(
"JSON forbids NaN and infinities: $value at path ${reader.path}"
)
}
return value
}
override fun toJson(writer: JsonWriter, value: Float?) {
// Manual null pointer check for the float.class adapter.
if (value == null) {
throw NullPointerException()
}
// Use the Number overload so we write out float precision instead of double precision.
writer.value(value)
}
override fun toString() = "JsonAdapter(Float)"
}
private val INTEGER_JSON_ADAPTER: JsonAdapter<Int> = object : JsonAdapter<Int>() {
override fun fromJson(reader: JsonReader): Int {
return reader.nextInt()
}
override fun toJson(writer: JsonWriter, value: Int?) {
writer.value(knownNotNull(value).toInt().toLong())
}
override fun toString() = "JsonAdapter(Integer)"
}
private val LONG_JSON_ADAPTER: JsonAdapter<Long> = object : JsonAdapter<Long>() {
override fun fromJson(reader: JsonReader): Long {
return reader.nextLong()
}
override fun toJson(writer: JsonWriter, value: Long?) {
writer.value(knownNotNull(value).toLong())
}
override fun toString() = "JsonAdapter(Long)"
}
private val SHORT_JSON_ADAPTER: JsonAdapter<Short> = object : JsonAdapter<Short>() {
override fun fromJson(reader: JsonReader): Short {
return rangeCheckNextInt(reader, "a short", Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort()
}
override fun toJson(writer: JsonWriter, value: Short?) {
writer.value(knownNotNull(value).toInt().toLong())
}
override fun toString() = "JsonAdapter(Short)"
}
private val STRING_JSON_ADAPTER: JsonAdapter<String> = object : JsonAdapter<String>() {
override fun fromJson(reader: JsonReader): String? {
return reader.nextString()
}
override fun toJson(writer: JsonWriter, value: String?) {
writer.value(value)
}
override fun toString() = "JsonAdapter(String)"
}
internal class EnumJsonAdapter<T : Enum<T>>(private val enumType: Class<T>) : JsonAdapter<T>() {
private val constants: Array<T> = enumType.enumConstants
private val nameStrings: Array<String> = Array(constants.size) { i ->
val constant = constants[i]
val constantName = constant.name
try {
enumType.getField(constantName).jsonName(constantName)
} catch (e: NoSuchFieldException) {
throw AssertionError("Missing field in ${enumType.name}", e)
}
}
private var options: JsonReader.Options = JsonReader.Options.of(*nameStrings)
override fun fromJson(reader: JsonReader): T {
val index = reader.selectString(options)
if (index != -1) return constants[index]
// We can consume the string safely, we are terminating anyway.
val path = reader.path
val name = reader.nextString()
throw JsonDataException(
"Expected one of ${nameStrings.toList()} but was $name at path $path"
)
}
override fun toJson(writer: JsonWriter, value: T?) {
writer.value(nameStrings[knownNotNull(value).ordinal])
}
override fun toString() = "JsonAdapter(${enumType.name})"
}
/**
* This adapter is used when the declared type is [Any]. Typically the runtime
* type is something else, and when encoding JSON this delegates to the runtime type's adapter.
* For decoding (where there is no runtime type to inspect), this uses maps and lists.
*
* This adapter needs a Moshi instance to look up the appropriate adapter for runtime types as
* they are encountered.
*/
internal class ObjectJsonAdapter(private val moshi: Moshi) : JsonAdapter<Any>() {
private val listJsonAdapter: JsonAdapter<List<*>> = moshi.adapter(List::class.java)
private val mapAdapter: JsonAdapter<Map<*, *>> = moshi.adapter(Map::class.java)
private val stringAdapter: JsonAdapter<String> = moshi.adapter(String::class.java)
private val doubleAdapter: JsonAdapter<Double> = moshi.adapter(Double::class.java)
private val booleanAdapter: JsonAdapter<Boolean> = moshi.adapter(Boolean::class.java)
override fun fromJson(reader: JsonReader): Any? {
return when (reader.peek()) {
JsonReader.Token.BEGIN_ARRAY -> listJsonAdapter.fromJson(reader)
JsonReader.Token.BEGIN_OBJECT -> mapAdapter.fromJson(reader)
JsonReader.Token.STRING -> stringAdapter.fromJson(reader)
JsonReader.Token.NUMBER -> doubleAdapter.fromJson(reader)
JsonReader.Token.BOOLEAN -> booleanAdapter.fromJson(reader)
JsonReader.Token.NULL -> reader.nextNull()
else -> throw IllegalStateException(
"Expected a value but was ${reader.peek()} at path ${reader.path}"
)
}
}
override fun toJson(writer: JsonWriter, value: Any?) {
val valueClass: Class<*> = knownNotNull(value).javaClass
if (valueClass == Any::class.java) {
// Don't recurse infinitely when the runtime type is also Object.class.
writer.beginObject()
writer.endObject()
} else {
moshi.adapter<Any>(toJsonType(valueClass), NO_ANNOTATIONS).toJson(writer, value)
}
}
/**
* Returns the type to look up a type adapter for when writing `value` to JSON. Without
* this, attempts to emit standard types like `LinkedHashMap` would fail because Moshi doesn't
* provide built-in adapters for implementation types. It knows how to **write**
* those types, but lacks a mechanism to read them because it doesn't know how to find the
* appropriate constructor.
*/
private fun toJsonType(valueClass: Class<*>): Class<*> {
if (Map::class.java.isAssignableFrom(valueClass)) return Map::class.java
return if (Collection::class.java.isAssignableFrom(valueClass)) Collection::class.java else valueClass
}
override fun toString() = "JsonAdapter(Object)"
}
}

View File

@@ -1160,7 +1160,10 @@ public final class MoshiTest {
public void qualifierWithElementsMayNotBeDirectlyRegistered() throws IOException {
try {
new Moshi.Builder()
.add(Boolean.class, Localized.class, StandardJsonAdapters.BOOLEAN_JSON_ADAPTER);
.add(
Boolean.class,
Localized.class,
StandardJsonAdapters.INSTANCE.getBOOLEAN_JSON_ADAPTER());
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected)