Convert JsonValueWriter to Kotlin (#1491)

* Rename .java to .kt

* Convert JsonValueWriter to Kotlin

* Use knownNotNull

* when of whens

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

* Nix the return

* Clean up

Co-authored-by: Parth Padgaonkar <1294660+JvmName@users.noreply.github.com>
This commit is contained in:
Zac Sweers
2022-01-10 12:21:14 -05:00
committed by GitHub
parent 6e81499501
commit 59afd4bb9b
2 changed files with 257 additions and 340 deletions

View File

@@ -1,340 +0,0 @@
/*
* Copyright (C) 2017 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.JsonScope.EMPTY_ARRAY;
import static com.squareup.moshi.JsonScope.EMPTY_DOCUMENT;
import static com.squareup.moshi.JsonScope.EMPTY_OBJECT;
import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT;
import static com.squareup.moshi.JsonScope.STREAMING_VALUE;
import static java.lang.Double.NEGATIVE_INFINITY;
import static java.lang.Double.POSITIVE_INFINITY;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import okio.Buffer;
import okio.BufferedSink;
import okio.ForwardingSink;
import okio.Okio;
/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */
final class JsonValueWriter extends JsonWriter {
Object[] stack = new Object[32];
private @Nullable String deferredName;
JsonValueWriter() {
pushScope(EMPTY_DOCUMENT);
}
public Object root() {
int size = stackSize;
if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) {
throw new IllegalStateException("Incomplete document");
}
return stack[0];
}
@Override
public JsonWriter beginArray() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Array cannot be used as a map key in JSON at path " + getPath());
}
if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_ARRAY) {
// Cancel this open. Invert the flatten stack size until this is closed.
flattenStackSize = ~flattenStackSize;
return this;
}
checkStack();
List<Object> list = new ArrayList<>();
add(list);
stack[stackSize] = list;
pathIndices[stackSize] = 0;
pushScope(EMPTY_ARRAY);
return this;
}
@Override
public JsonWriter endArray() throws IOException {
if (peekScope() != EMPTY_ARRAY) {
throw new IllegalStateException("Nesting problem.");
}
if (stackSize == ~flattenStackSize) {
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
flattenStackSize = ~flattenStackSize;
return this;
}
stackSize--;
stack[stackSize] = null;
pathIndices[stackSize - 1]++;
return this;
}
@Override
public JsonWriter beginObject() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Object cannot be used as a map key in JSON at path " + getPath());
}
if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_OBJECT) {
// Cancel this open. Invert the flatten stack size until this is closed.
flattenStackSize = ~flattenStackSize;
return this;
}
checkStack();
Map<String, Object> map = new LinkedHashTreeMap<>();
add(map);
stack[stackSize] = map;
pushScope(EMPTY_OBJECT);
return this;
}
@Override
public JsonWriter endObject() throws IOException {
if (peekScope() != EMPTY_OBJECT) {
throw new IllegalStateException("Nesting problem.");
}
if (deferredName != null) {
throw new IllegalStateException("Dangling name: " + deferredName);
}
if (stackSize == ~flattenStackSize) {
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
flattenStackSize = ~flattenStackSize;
return this;
}
promoteValueToName = false;
stackSize--;
stack[stackSize] = null;
pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
pathIndices[stackSize - 1]++;
return this;
}
@Override
public JsonWriter name(String name) throws IOException {
if (name == null) {
throw new NullPointerException("name == null");
}
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
if (peekScope() != EMPTY_OBJECT || deferredName != null || promoteValueToName) {
throw new IllegalStateException("Nesting problem.");
}
deferredName = name;
pathNames[stackSize - 1] = name;
return this;
}
@Override
public JsonWriter value(@Nullable String value) throws IOException {
if (promoteValueToName) {
promoteValueToName = false;
return name(value);
}
add(value);
pathIndices[stackSize - 1]++;
return this;
}
@Override
public JsonWriter nullValue() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"null cannot be used as a map key in JSON at path " + getPath());
}
add(null);
pathIndices[stackSize - 1]++;
return this;
}
@Override
public JsonWriter value(boolean value) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Boolean cannot be used as a map key in JSON at path " + getPath());
}
add(value);
pathIndices[stackSize - 1]++;
return this;
}
@Override
public JsonWriter value(@Nullable Boolean value) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Boolean cannot be used as a map key in JSON at path " + getPath());
}
add(value);
pathIndices[stackSize - 1]++;
return this;
}
@Override
public JsonWriter value(double value) throws IOException {
if (!lenient
&& (Double.isNaN(value) || value == NEGATIVE_INFINITY || value == POSITIVE_INFINITY)) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
}
if (promoteValueToName) {
promoteValueToName = false;
return name(Double.toString(value));
}
add(value);
pathIndices[stackSize - 1]++;
return this;
}
@Override
public JsonWriter value(long value) throws IOException {
if (promoteValueToName) {
promoteValueToName = false;
return name(Long.toString(value));
}
add(value);
pathIndices[stackSize - 1]++;
return this;
}
@Override
public JsonWriter value(@Nullable Number value) throws IOException {
// If it's trivially converted to a long, do that.
if (value instanceof Byte
|| value instanceof Short
|| value instanceof Integer
|| value instanceof Long) {
return value(value.longValue());
}
// If it's trivially converted to a double, do that.
if (value instanceof Float || value instanceof Double) {
return value(value.doubleValue());
}
if (value == null) {
return nullValue();
}
// Everything else gets converted to a BigDecimal.
BigDecimal bigDecimalValue =
value instanceof BigDecimal ? ((BigDecimal) value) : new BigDecimal(value.toString());
if (promoteValueToName) {
promoteValueToName = false;
return name(bigDecimalValue.toString());
}
add(bigDecimalValue);
pathIndices[stackSize - 1]++;
return this;
}
@Override
public BufferedSink valueSink() {
if (promoteValueToName) {
throw new IllegalStateException(
"BufferedSink cannot be used as a map key in JSON at path " + getPath());
}
if (peekScope() == STREAMING_VALUE) {
throw new IllegalStateException("Sink from valueSink() was not closed");
}
pushScope(STREAMING_VALUE);
final Buffer buffer = new Buffer();
return Okio.buffer(
new ForwardingSink(buffer) {
@Override
public void close() throws IOException {
if (peekScope() != STREAMING_VALUE || stack[stackSize] != null) {
throw new AssertionError();
}
stackSize--; // Remove STREAMING_VALUE from the stack.
Object value = JsonReader.of(buffer).readJsonValue();
boolean serializeNulls = JsonValueWriter.this.serializeNulls;
JsonValueWriter.this.serializeNulls = true;
try {
add(value);
} finally {
JsonValueWriter.this.serializeNulls = serializeNulls;
}
pathIndices[stackSize - 1]++;
}
});
}
@Override
public void close() throws IOException {
int size = stackSize;
if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) {
throw new IOException("Incomplete document");
}
stackSize = 0;
}
@Override
public void flush() throws IOException {
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
}
private JsonValueWriter add(@Nullable Object newTop) {
int scope = peekScope();
if (stackSize == 1) {
if (scope != EMPTY_DOCUMENT) {
throw new IllegalStateException("JSON must have only one top-level value.");
}
scopes[stackSize - 1] = NONEMPTY_DOCUMENT;
stack[stackSize - 1] = newTop;
} else if (scope == EMPTY_OBJECT && deferredName != null) {
if (newTop != null || serializeNulls) {
@SuppressWarnings("unchecked") // Our maps always have string keys and object values.
Map<String, Object> map = (Map<String, Object>) stack[stackSize - 1];
Object replaced = map.put(deferredName, newTop);
if (replaced != null) {
throw new IllegalArgumentException(
"Map key '"
+ deferredName
+ "' has multiple values at path "
+ getPath()
+ ": "
+ replaced
+ " and "
+ newTop);
}
}
deferredName = null;
} else if (scope == EMPTY_ARRAY) {
@SuppressWarnings("unchecked") // Our lists always have object values.
List<Object> list = (List<Object>) stack[stackSize - 1];
list.add(newTop);
} else if (scope == STREAMING_VALUE) {
throw new IllegalStateException("Sink from valueSink() was not closed");
} else {
throw new IllegalStateException("Nesting problem.");
}
return this;
}
}

View File

@@ -0,0 +1,257 @@
/*
* Copyright (C) 2017 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.JsonScope.EMPTY_ARRAY
import com.squareup.moshi.JsonScope.EMPTY_DOCUMENT
import com.squareup.moshi.JsonScope.EMPTY_OBJECT
import com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT
import com.squareup.moshi.JsonScope.STREAMING_VALUE
import com.squareup.moshi.internal.knownNotNull
import okio.Buffer
import okio.BufferedSink
import okio.ForwardingSink
import okio.IOException
import okio.buffer
import java.math.BigDecimal
/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */
internal class JsonValueWriter : JsonWriter() {
@JvmField // TODO remove once JsonWriter is Kotlin
var stack = arrayOfNulls<Any>(32)
private var deferredName: String? = null
init {
pushScope(EMPTY_DOCUMENT)
}
fun root(): Any? {
val size = stackSize
check(size <= 1 && (size != 1 || scopes[0] == NONEMPTY_DOCUMENT)) { "Incomplete document" }
return stack[0]
}
override fun beginArray(): JsonWriter {
check(!promoteValueToName) { "Array cannot be used as a map key in JSON at path $path" }
if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_ARRAY) {
// Cancel this open. Invert the flatten stack size until this is closed.
flattenStackSize = flattenStackSize.inv()
return this
}
checkStack()
val list = mutableListOf<Any>()
add(list)
stack[stackSize] = list
pathIndices[stackSize] = 0
pushScope(EMPTY_ARRAY)
return this
}
override fun endArray(): JsonWriter {
check(peekScope() == EMPTY_ARRAY) { "Nesting problem." }
if (stackSize == flattenStackSize.inv()) {
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
flattenStackSize = flattenStackSize.inv()
return this
}
stackSize--
stack[stackSize] = null
pathIndices[stackSize - 1]++
return this
}
override fun beginObject(): JsonWriter {
check(!promoteValueToName) { "Object cannot be used as a map key in JSON at path $path" }
if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_OBJECT) {
// Cancel this open. Invert the flatten stack size until this is closed.
flattenStackSize = flattenStackSize.inv()
return this
}
checkStack()
val map = LinkedHashTreeMap<String, Any>()
add(map)
stack[stackSize] = map
pushScope(EMPTY_OBJECT)
return this
}
override fun endObject(): JsonWriter {
check(peekScope() == EMPTY_OBJECT) { "Nesting problem." }
check(deferredName == null) { "Dangling name: $deferredName" }
if (stackSize == flattenStackSize.inv()) {
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
flattenStackSize = flattenStackSize.inv()
return this
}
promoteValueToName = false
stackSize--
stack[stackSize] = null
pathNames[stackSize] = null // Free the last path name so that it can be garbage collected!
pathIndices[stackSize - 1]++
return this
}
override fun name(name: String): JsonWriter {
check(stackSize != 0) { "JsonWriter is closed." }
check(peekScope() == EMPTY_OBJECT && deferredName == null && !promoteValueToName) { "Nesting problem." }
deferredName = name
pathNames[stackSize - 1] = name
return this
}
override fun value(value: String?): JsonWriter {
if (promoteValueToName) {
promoteValueToName = false
return name(value!!)
}
add(value)
pathIndices[stackSize - 1]++
return this
}
override fun nullValue(): JsonWriter {
check(!promoteValueToName) { "null cannot be used as a map key in JSON at path $path" }
add(null)
pathIndices[stackSize - 1]++
return this
}
override fun value(value: Boolean): JsonWriter {
check(!promoteValueToName) { "Boolean cannot be used as a map key in JSON at path $path" }
add(value)
pathIndices[stackSize - 1]++
return this
}
override fun value(value: Boolean?): JsonWriter {
check(!promoteValueToName) { "Boolean cannot be used as a map key in JSON at path $path" }
add(value)
pathIndices[stackSize - 1]++
return this
}
override fun value(value: Double): JsonWriter {
require(lenient || !value.isNaN() && value != Double.NEGATIVE_INFINITY && value != Double.POSITIVE_INFINITY) {
"Numeric values must be finite, but was $value"
}
if (promoteValueToName) {
promoteValueToName = false
return name(value.toString())
}
add(value)
pathIndices[stackSize - 1]++
return this
}
override fun value(value: Long): JsonWriter {
if (promoteValueToName) {
promoteValueToName = false
return name(value.toString())
}
add(value)
pathIndices[stackSize - 1]++
return this
}
override fun value(value: Number?): JsonWriter = apply {
when (value) {
null -> nullValue()
// If it's trivially converted to a long, do that.
is Byte, is Short, is Int, is Long -> value(value.toLong())
// If it's trivially converted to a double, do that.
is Float, is Double -> value(value.toDouble())
else -> {
// Everything else gets converted to a BigDecimal.
val bigDecimalValue = if (value is BigDecimal) value else BigDecimal(value.toString())
if (promoteValueToName) {
promoteValueToName = false
return name(bigDecimalValue.toString())
}
add(bigDecimalValue)
pathIndices[stackSize - 1]++
}
}
}
override fun valueSink(): BufferedSink {
check(!promoteValueToName) { "BufferedSink cannot be used as a map key in JSON at path $path" }
check(peekScope() != STREAMING_VALUE) { "Sink from valueSink() was not closed" }
pushScope(STREAMING_VALUE)
val buffer = Buffer()
return object : ForwardingSink(buffer) {
override fun close() {
if (peekScope() != STREAMING_VALUE || stack[stackSize] != null) {
throw AssertionError()
}
stackSize-- // Remove STREAMING_VALUE from the stack.
val value = JsonReader.of(buffer).readJsonValue()
val serializeNulls = serializeNulls
this@JsonValueWriter.serializeNulls = true
try {
add(value)
} finally {
this@JsonValueWriter.serializeNulls = serializeNulls
}
pathIndices[stackSize - 1]++
}
}.buffer()
}
override fun close() {
val size = stackSize
if (size > 1 || size == 1 && scopes[0] != NONEMPTY_DOCUMENT) {
throw IOException("Incomplete document")
}
stackSize = 0
}
override fun flush() {
check(stackSize != 0) { "JsonWriter is closed." }
}
private fun add(newTop: Any?): JsonValueWriter {
val scope = peekScope()
when {
stackSize == 1 -> {
check(scope == EMPTY_DOCUMENT) { "JSON must have only one top-level value." }
scopes[stackSize - 1] = NONEMPTY_DOCUMENT
stack[stackSize - 1] = newTop
}
scope == EMPTY_OBJECT && deferredName != null -> {
if (newTop != null || serializeNulls) {
// Our maps always have string keys and object values.
@Suppress("UNCHECKED_CAST")
val map = stack[stackSize - 1] as MutableMap<String, Any?>
// Safe to assume not null as this is single-threaded and smartcast just can't handle it
val replaced = map.put(knownNotNull(deferredName), newTop)
require(replaced == null) {
"Map key '$deferredName' has multiple values at path $path: $replaced and $newTop"
}
}
deferredName = null
}
scope == EMPTY_ARRAY -> {
// Our lists always have object values.
@Suppress("UNCHECKED_CAST")
val list = stack[stackSize - 1] as MutableList<Any?>
list.add(newTop)
}
scope == STREAMING_VALUE -> throw IllegalStateException("Sink from valueSink() was not closed")
else -> throw IllegalStateException("Nesting problem.")
}
return this
}
}