From 323d97c787df3b79b28916f8a4cbbf22d59e3992 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Mon, 10 Jan 2022 11:29:37 -0500 Subject: [PATCH] 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 * When Co-authored-by: Parth Padgaonkar <1294660+JvmName@users.noreply.github.com> * Another when * Spotless Co-authored-by: Egor Andreevich Co-authored-by: Parth Padgaonkar <1294660+JvmName@users.noreply.github.com> --- .../adapters/PolymorphicJsonAdapterFactory.kt | 2 +- .../moshi/kotlin/reflect/KotlinJsonAdapter.kt | 2 +- moshi/japicmp/build.gradle.kts | 3 + .../java/com/squareup/moshi/JsonAdapter.java | 332 ------------------ .../java/com/squareup/moshi/JsonAdapter.kt | 296 ++++++++++++++++ .../com/squareup/moshi/JsonAdapterTest.java | 2 +- 6 files changed, 302 insertions(+), 335 deletions(-) delete mode 100644 moshi/src/main/java/com/squareup/moshi/JsonAdapter.java create mode 100644 moshi/src/main/java/com/squareup/moshi/JsonAdapter.kt diff --git a/moshi-adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.kt b/moshi-adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.kt index 471ef04..0de1ce3 100644 --- a/moshi-adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.kt +++ b/moshi-adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.kt @@ -167,7 +167,7 @@ public class PolymorphicJsonAdapterFactory internal constructor( } } - override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { + override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (type.rawType != baseType || annotations.isNotEmpty()) { return null } diff --git a/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt b/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt index 11ff644..d21f2d9 100644 --- a/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt +++ b/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt @@ -190,7 +190,7 @@ internal class KotlinJsonAdapter( } public class KotlinJsonAdapterFactory : JsonAdapter.Factory { - override fun create(type: Type, annotations: MutableSet, moshi: Moshi): + override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (annotations.isNotEmpty()) return null diff --git a/moshi/japicmp/build.gradle.kts b/moshi/japicmp/build.gradle.kts index a7c7433..a584185 100644 --- a/moshi/japicmp/build.gradle.kts +++ b/moshi/japicmp/build.gradle.kts @@ -30,6 +30,9 @@ val japicmp = tasks.register("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 ) diff --git a/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java deleted file mode 100644 index 4cf1180..0000000 --- a/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java +++ /dev/null @@ -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. - * - *

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 { - - /** - * 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. - * - *

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 serializeNulls() { - final JsonAdapter delegate = this; - return new JsonAdapter() { - @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 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}. - * - *

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 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 lenient() { - final JsonAdapter delegate = this; - return new JsonAdapter() { - @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 failOnUnknown() { - final JsonAdapter delegate = this; - return new JsonAdapter() { - @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 indent(final String indent) { - if (indent == null) { - throw new NullPointerException("indent == null"); - } - final JsonAdapter delegate = this; - return new JsonAdapter() { - @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. - * - *

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 annotations, Moshi moshi); - } -} diff --git a/moshi/src/main/java/com/squareup/moshi/JsonAdapter.kt b/moshi/src/main/java/com/squareup/moshi/JsonAdapter.kt new file mode 100644 index 0000000..7c099ae --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/JsonAdapter.kt @@ -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 { + /** + * 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 { + val delegate: JsonAdapter = this + return object : JsonAdapter() { + 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 { + 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 { + 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 { + val delegate: JsonAdapter = this + return object : JsonAdapter() { + 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 { + val delegate: JsonAdapter = this + return object : JsonAdapter() { + 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 { + val delegate: JsonAdapter = this + return object : JsonAdapter() { + 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, moshi: Moshi): JsonAdapter<*>? + } +} diff --git a/moshi/src/test/java/com/squareup/moshi/JsonAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/JsonAdapterTest.java index c0563fd..c37042c 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonAdapterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonAdapterTest.java @@ -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"); } }