Convert JsonAdapter to Kotlin (#1475)

* Rename .java to .kt

* Convert JsonAdapter to Kotlin

Note that there's more to be done here I think, namely exploring removing the NonNull adapter and making the nullSafe() adapter public so that nullability is directly in the API. Saving that for another day though

* Update a couple usages

* Fix override

* Add exclusion for open

* Add `@Language` annotation for json strings

Allows the IDE to automatically make this pretty

* Spotless

* Nullable

Co-authored-by: Egor Andreevich <egor@squareup.com>

* When

Co-authored-by: Parth Padgaonkar <1294660+JvmName@users.noreply.github.com>

* Another when

* Spotless

Co-authored-by: Egor Andreevich <egor@squareup.com>
Co-authored-by: Parth Padgaonkar <1294660+JvmName@users.noreply.github.com>
This commit is contained in:
Zac Sweers
2022-01-10 11:29:37 -05:00
committed by GitHub
parent 6f8d690e6e
commit 323d97c787
6 changed files with 302 additions and 335 deletions

View File

@@ -167,7 +167,7 @@ public class PolymorphicJsonAdapterFactory<T> internal constructor(
}
}
override fun create(type: Type, annotations: Set<Annotation?>, moshi: Moshi): JsonAdapter<*>? {
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
if (type.rawType != baseType || annotations.isNotEmpty()) {
return null
}

View File

@@ -190,7 +190,7 @@ internal class KotlinJsonAdapter<T>(
}
public class KotlinJsonAdapterFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi):
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi):
JsonAdapter<*>? {
if (annotations.isNotEmpty()) return null

View File

@@ -30,6 +30,9 @@ val japicmp = tasks.register<JapicmpTask>("japicmp") {
"com.squareup.moshi.internal.NullSafeJsonAdapter", // Internal.
"com.squareup.moshi.internal.Util" // Internal.
)
methodExcludes = listOf(
"com.squareup.moshi.JsonAdapter#indent(java.lang.String)" // Was unintentionally open before
)
fieldExcludes = listOf(
"com.squareup.moshi.CollectionJsonAdapter#FACTORY" // False-positive, class is not public anyway
)

View File

@@ -1,332 +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 com.squareup.moshi.internal.NonNullJsonAdapter;
import com.squareup.moshi.internal.NullSafeJsonAdapter;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Set;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
/**
* Converts Java values to JSON, and JSON values to Java.
*
* <p>JsonAdapter instances provided by Moshi are thread-safe, meaning multiple threads can safely
* use a single instance concurrently.
*
* <p>Custom JsonAdapter implementations should be designed to be thread-safe.
*/
public abstract class JsonAdapter<T> {
/**
* Decodes a nullable instance of type {@link T} from the given {@code reader}.
*
* @throws JsonDataException when the data in a JSON document doesn't match the data expected by
* the caller.
*/
@CheckReturnValue
public abstract @Nullable T fromJson(JsonReader reader) throws IOException;
/**
* Decodes a nullable instance of type {@link T} from the given {@code source}.
*
* @throws JsonDataException when the data in a JSON document doesn't match the data expected by
* the caller.
*/
@CheckReturnValue
public final @Nullable T fromJson(BufferedSource source) throws IOException {
return fromJson(JsonReader.of(source));
}
/**
* Decodes a nullable instance of type {@link T} from the given {@code string}.
*
* @throws JsonDataException when the data in a JSON document doesn't match the data expected by
* the caller.
*/
@CheckReturnValue
public final @Nullable T fromJson(String string) throws IOException {
JsonReader reader = JsonReader.of(new Buffer().writeUtf8(string));
T result = fromJson(reader);
if (!isLenient() && reader.peek() != JsonReader.Token.END_DOCUMENT) {
throw new JsonDataException("JSON document was not fully consumed.");
}
return result;
}
/** Encodes the given {@code value} with the given {@code writer}. */
public abstract void toJson(JsonWriter writer, @Nullable T value) throws IOException;
public final void toJson(BufferedSink sink, @Nullable T value) throws IOException {
JsonWriter writer = JsonWriter.of(sink);
toJson(writer, value);
}
/** Encodes the given {@code value} into a String and returns it. */
@CheckReturnValue
public final String toJson(@Nullable T value) {
Buffer buffer = new Buffer();
try {
toJson(buffer, value);
} catch (IOException e) {
throw new AssertionError(e); // No I/O writing to a Buffer.
}
return buffer.readUtf8();
}
/**
* Encodes {@code value} as a Java value object comprised of maps, lists, strings, numbers,
* booleans, and nulls.
*
* <p>Values encoded using {@code value(double)} or {@code value(long)} are modeled with the
* corresponding boxed type. Values encoded using {@code value(Number)} are modeled as a {@link
* Long} for boxed integer types ({@link Byte}, {@link Short}, {@link Integer}, and {@link Long}),
* as a {@link Double} for boxed floating point types ({@link Float} and {@link Double}), and as a
* {@link BigDecimal} for all other types.
*/
@CheckReturnValue
public final @Nullable Object toJsonValue(@Nullable T value) {
JsonValueWriter writer = new JsonValueWriter();
try {
toJson(writer, value);
return writer.root();
} catch (IOException e) {
throw new AssertionError(e); // No I/O writing to an object.
}
}
/**
* Decodes a Java value object from {@code value}, which must be comprised of maps, lists,
* strings, numbers, booleans and nulls.
*/
@CheckReturnValue
public final @Nullable T fromJsonValue(@Nullable Object value) {
JsonValueReader reader = new JsonValueReader(value);
try {
return fromJson(reader);
} catch (IOException e) {
throw new AssertionError(e); // No I/O reading from an object.
}
}
/**
* Returns a JSON adapter equal to this JSON adapter, but that serializes nulls when encoding
* JSON.
*/
@CheckReturnValue
public final JsonAdapter<T> serializeNulls() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override
public @Nullable T fromJson(JsonReader reader) throws IOException {
return delegate.fromJson(reader);
}
@Override
public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
boolean serializeNulls = writer.getSerializeNulls();
writer.setSerializeNulls(true);
try {
delegate.toJson(writer, value);
} finally {
writer.setSerializeNulls(serializeNulls);
}
}
@Override
boolean isLenient() {
return delegate.isLenient();
}
@Override
public String toString() {
return delegate + ".serializeNulls()";
}
};
}
/**
* Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing
* nulls.
*/
@CheckReturnValue
public final JsonAdapter<T> nullSafe() {
if (this instanceof NullSafeJsonAdapter) {
return this;
}
return new NullSafeJsonAdapter<>(this);
}
/**
* Returns a JSON adapter equal to this JSON adapter, but that refuses null values. If null is
* read or written this will throw a {@link JsonDataException}.
*
* <p>Note that this adapter will not usually be invoked for absent values and so those must be
* handled elsewhere. This should only be used to fail on explicit nulls.
*/
@CheckReturnValue
public final JsonAdapter<T> nonNull() {
if (this instanceof NonNullJsonAdapter) {
return this;
}
return new NonNullJsonAdapter<>(this);
}
/** Returns a JSON adapter equal to this, but is lenient when reading and writing. */
@CheckReturnValue
public final JsonAdapter<T> lenient() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override
public @Nullable T fromJson(JsonReader reader) throws IOException {
boolean lenient = reader.isLenient();
reader.setLenient(true);
try {
return delegate.fromJson(reader);
} finally {
reader.setLenient(lenient);
}
}
@Override
public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
boolean lenient = writer.isLenient();
writer.setLenient(true);
try {
delegate.toJson(writer, value);
} finally {
writer.setLenient(lenient);
}
}
@Override
boolean isLenient() {
return true;
}
@Override
public String toString() {
return delegate + ".lenient()";
}
};
}
/**
* Returns a JSON adapter equal to this, but that throws a {@link JsonDataException} when
* {@linkplain JsonReader#setFailOnUnknown(boolean) unknown names and values} are encountered.
* This constraint applies to both the top-level message handled by this type adapter as well as
* to nested messages.
*/
@CheckReturnValue
public final JsonAdapter<T> failOnUnknown() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override
public @Nullable T fromJson(JsonReader reader) throws IOException {
boolean skipForbidden = reader.failOnUnknown();
reader.setFailOnUnknown(true);
try {
return delegate.fromJson(reader);
} finally {
reader.setFailOnUnknown(skipForbidden);
}
}
@Override
public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
delegate.toJson(writer, value);
}
@Override
boolean isLenient() {
return delegate.isLenient();
}
@Override
public String toString() {
return delegate + ".failOnUnknown()";
}
};
}
/**
* Return a JSON adapter equal to this, but using {@code indent} to control how the result is
* formatted. The {@code indent} string to be repeated for each level of indentation in the
* encoded document. If {@code indent.isEmpty()} the encoded document will be compact. Otherwise
* the encoded document will be more human-readable.
*
* @param indent a string containing only whitespace.
*/
@CheckReturnValue
public JsonAdapter<T> indent(final String indent) {
if (indent == null) {
throw new NullPointerException("indent == null");
}
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override
public @Nullable T fromJson(JsonReader reader) throws IOException {
return delegate.fromJson(reader);
}
@Override
public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
String originalIndent = writer.getIndent();
writer.setIndent(indent);
try {
delegate.toJson(writer, value);
} finally {
writer.setIndent(originalIndent);
}
}
@Override
boolean isLenient() {
return delegate.isLenient();
}
@Override
public String toString() {
return delegate + ".indent(\"" + indent + "\")";
}
};
}
boolean isLenient() {
return false;
}
public interface Factory {
/**
* Attempts to create an adapter for {@code type} annotated with {@code annotations}. This
* returns the adapter if one was created, or null if this factory isn't capable of creating
* such an adapter.
*
* <p>Implementations may use {@link Moshi#adapter} to compose adapters of other types, or
* {@link Moshi#nextAdapter} to delegate to the underlying adapter of the same type.
*/
@CheckReturnValue
@Nullable
JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi);
}
}

View File

@@ -0,0 +1,296 @@
/*
* 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.NonNullJsonAdapter
import com.squareup.moshi.internal.NullSafeJsonAdapter
import okio.Buffer
import okio.BufferedSink
import okio.BufferedSource
import okio.IOException
import org.intellij.lang.annotations.Language
import java.lang.reflect.Type
import javax.annotation.CheckReturnValue
import kotlin.Throws
/**
* Converts Java values to JSON, and JSON values to Java.
*
* JsonAdapter instances provided by Moshi are thread-safe, meaning multiple threads can safely
* use a single instance concurrently.
*
* Custom JsonAdapter implementations should be designed to be thread-safe.
*/
public abstract class JsonAdapter<T> {
/**
* Decodes a nullable instance of type [T] from the given [reader].
*
* @throws JsonDataException when the data in a JSON document doesn't match the data expected by
* the caller.
*/
@CheckReturnValue
@Throws(IOException::class)
public abstract fun fromJson(reader: JsonReader): T?
/**
* Decodes a nullable instance of type [T] from the given [source].
*
* @throws JsonDataException when the data in a JSON document doesn't match the data expected by
* the caller.
*/
@CheckReturnValue
@Throws(IOException::class)
public fun fromJson(source: BufferedSource): T? = fromJson(JsonReader.of(source))
/**
* Decodes a nullable instance of type [T] from the given `string`.
*
* @throws JsonDataException when the data in a JSON document doesn't match the data expected by
* the caller.
*/
@CheckReturnValue
@Throws(IOException::class)
public fun fromJson(@Language("JSON") string: String): T? {
val reader = JsonReader.of(Buffer().writeUtf8(string))
val result = fromJson(reader)
if (!isLenient && reader.peek() != JsonReader.Token.END_DOCUMENT) {
throw JsonDataException("JSON document was not fully consumed.")
}
return result
}
/** Encodes the given [value] with the given [writer]. */
@Throws(IOException::class)
public abstract fun toJson(writer: JsonWriter, value: T?)
@Throws(IOException::class)
public fun toJson(sink: BufferedSink, value: T?) {
val writer = JsonWriter.of(sink)
toJson(writer, value)
}
/** Encodes the given [value] into a String and returns it. */
@CheckReturnValue
public fun toJson(value: T?): String {
val buffer = Buffer()
try {
toJson(buffer, value)
} catch (e: IOException) {
throw AssertionError(e) // No I/O writing to a Buffer.
}
return buffer.readUtf8()
}
/**
* Encodes [value] as a Java value object comprised of maps, lists, strings, numbers,
* booleans, and nulls.
*
* Values encoded using `value(double)` or `value(long)` are modeled with the
* corresponding boxed type. Values encoded using `value(Number)` are modeled as a [Long] for boxed integer types
* ([Byte], [Short], [Integer], and [Long]), as a [Double] for boxed floating point types ([Float] and [Double]),
* and as a [java.math.BigDecimal] for all other types.
*/
@CheckReturnValue
public fun toJsonValue(value: T?): Any? {
val writer = JsonValueWriter()
return try {
toJson(writer, value)
writer.root()
} catch (e: IOException) {
throw AssertionError(e) // No I/O writing to an object.
}
}
/**
* Decodes a Java value object from [value], which must be comprised of maps, lists,
* strings, numbers, booleans and nulls.
*/
@CheckReturnValue
public fun fromJsonValue(value: Any?): T? {
val reader = JsonValueReader(value)
return try {
fromJson(reader)
} catch (e: IOException) {
throw AssertionError(e) // No I/O reading from an object.
}
}
/**
* Returns a JSON adapter equal to this JSON adapter, but that serializes nulls when encoding
* JSON.
*/
@CheckReturnValue
public fun serializeNulls(): JsonAdapter<T> {
val delegate: JsonAdapter<T> = this
return object : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader) = delegate.fromJson(reader)
override fun toJson(writer: JsonWriter, value: T?) {
val serializeNulls = writer.getSerializeNulls()
writer.setSerializeNulls(true)
try {
delegate.toJson(writer, value)
} finally {
writer.setSerializeNulls(serializeNulls)
}
}
override val isLenient: Boolean
get() = delegate.isLenient
override fun toString() = "$delegate.serializeNulls()"
}
}
/**
* Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing
* nulls.
*/
@CheckReturnValue
public fun nullSafe(): JsonAdapter<T> {
return when (this) {
is NullSafeJsonAdapter<*> -> this
else -> NullSafeJsonAdapter(this)
}
}
/**
* Returns a JSON adapter equal to this JSON adapter, but that refuses null values. If null is
* read or written this will throw a [JsonDataException].
*
* Note that this adapter will not usually be invoked for absent values and so those must be
* handled elsewhere. This should only be used to fail on explicit nulls.
*/
@CheckReturnValue
public fun nonNull(): JsonAdapter<T> {
return when (this) {
is NonNullJsonAdapter<*> -> this
else -> NonNullJsonAdapter(this)
}
}
/** Returns a JSON adapter equal to this, but is lenient when reading and writing. */
@CheckReturnValue
public fun lenient(): JsonAdapter<T> {
val delegate: JsonAdapter<T> = this
return object : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader): T? {
val lenient = reader.isLenient
reader.isLenient = true
return try {
delegate.fromJson(reader)
} finally {
reader.isLenient = lenient
}
}
override fun toJson(writer: JsonWriter, value: T?) {
val lenient = writer.isLenient
writer.isLenient = true
try {
delegate.toJson(writer, value)
} finally {
writer.isLenient = lenient
}
}
override val isLenient: Boolean
get() = true
override fun toString() = "$delegate.lenient()"
}
}
/**
* Returns a JSON adapter equal to this, but that throws a [JsonDataException] when
* [unknown names and values][JsonReader.setFailOnUnknown] are encountered.
* This constraint applies to both the top-level message handled by this type adapter as well as
* to nested messages.
*/
@CheckReturnValue
public fun failOnUnknown(): JsonAdapter<T> {
val delegate: JsonAdapter<T> = this
return object : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader): T? {
val skipForbidden = reader.failOnUnknown()
reader.setFailOnUnknown(true)
return try {
delegate.fromJson(reader)
} finally {
reader.setFailOnUnknown(skipForbidden)
}
}
override fun toJson(writer: JsonWriter, value: T?) {
delegate.toJson(writer, value)
}
override val isLenient: Boolean
get() = delegate.isLenient
override fun toString() = "$delegate.failOnUnknown()"
}
}
/**
* Return a JSON adapter equal to this, but using `indent` to control how the result is
* formatted. The `indent` string to be repeated for each level of indentation in the
* encoded document. If `indent.isEmpty()` the encoded document will be compact. Otherwise
* the encoded document will be more human-readable.
*
* @param indent a string containing only whitespace.
*/
@CheckReturnValue
public fun indent(indent: String): JsonAdapter<T> {
val delegate: JsonAdapter<T> = this
return object : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader): T? {
return delegate.fromJson(reader)
}
override fun toJson(writer: JsonWriter, value: T?) {
val originalIndent = writer.getIndent()
writer.setIndent(indent)
try {
delegate.toJson(writer, value)
} finally {
writer.setIndent(originalIndent)
}
}
override val isLenient: Boolean
get() = delegate.isLenient
override fun toString() = "$delegate.indent(\"$indent\")"
}
}
public open val isLenient: Boolean
get() = false
public fun interface Factory {
/**
* Attempts to create an adapter for `type` annotated with `annotations`. This
* returns the adapter if one was created, or null if this factory isn't capable of creating
* such an adapter.
*
* Implementations may use [Moshi.adapter] to compose adapters of other types, or
* [Moshi.nextAdapter] to delegate to the underlying adapter of the same type.
*/
@CheckReturnValue
public fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>?
}
}

View File

@@ -219,7 +219,7 @@ public final class JsonAdapterTest {
adapter.indent(null);
fail();
} catch (NullPointerException expected) {
assertThat(expected).hasMessageThat().isEqualTo("indent == null");
assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null");
}
}