Merge pull request #232 from square/jwilson.0123.object_json_writer

Implement ObjectJsonWriter.
This commit is contained in:
Jake Wharton
2017-01-24 12:04:41 -05:00
committed by GitHub
10 changed files with 953 additions and 561 deletions

View File

@@ -188,7 +188,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
}
/**
* Returns the value on the top of the stack.
* Returns the scope on the top of the stack.
*/
private int peek() {
if (stackSize == 0) {
@@ -212,7 +212,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
throw new IllegalStateException("JsonWriter is closed.");
}
if (deferredName != null) {
throw new IllegalStateException();
throw new IllegalStateException("Nesting problem.");
}
deferredName = name;
pathNames[stackSize - 1] = name;

View File

@@ -69,7 +69,7 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
V replaced = result.put(name, value);
if (replaced != null) {
throw new JsonDataException("Map key '" + name + "' has multiple values at path "
+ reader.getPath());
+ reader.getPath() + ": " + replaced + " and " + value);
}
}
reader.endObject();

View File

@@ -0,0 +1,237 @@
/*
* 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
*
* http://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 java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
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;
/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */
final class ObjectJsonWriter extends JsonWriter {
private String indent;
private boolean lenient;
private boolean serializeNulls;
private final Object[] stack = new Object[32];
private final int[] scopes = new int[32];
private final String[] pathNames = new String[32];
private final int[] pathIndices = new int[32];
private int stackSize = 0;
private String deferredName;
ObjectJsonWriter() {
scopes[stackSize++] = 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 void setIndent(String indent) {
this.indent = indent;
}
@Override public String getIndent() {
return indent;
}
@Override public void setLenient(boolean lenient) {
this.lenient = lenient;
}
@Override public boolean isLenient() {
return lenient;
}
@Override public void setSerializeNulls(boolean serializeNulls) {
this.serializeNulls = serializeNulls;
}
@Override public boolean getSerializeNulls() {
return serializeNulls;
}
@Override public JsonWriter beginArray() throws IOException {
if (stackSize == stack.length) {
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
}
List<Object> list = new ArrayList<>();
add(list);
stack[stackSize] = list;
scopes[stackSize] = EMPTY_ARRAY;
pathIndices[stackSize] = 0;
stackSize++;
return this;
}
@Override public JsonWriter endArray() throws IOException {
if (peek() != EMPTY_ARRAY) {
throw new IllegalStateException("Nesting problem.");
}
stackSize--;
stack[stackSize] = null;
pathIndices[stackSize - 1]++;
return this;
}
@Override public JsonWriter beginObject() throws IOException {
if (stackSize == stack.length) {
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
}
Map<String, Object> map = new LinkedHashTreeMap<>();
add(map);
stack[stackSize] = map;
scopes[stackSize] = EMPTY_OBJECT;
stackSize++;
return this;
}
@Override public JsonWriter endObject() throws IOException {
if (peek() != EMPTY_OBJECT || deferredName != null) {
throw new IllegalStateException("Nesting problem.");
}
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 (peek() != EMPTY_OBJECT || deferredName != null) {
throw new IllegalStateException("Nesting problem.");
}
pathNames[stackSize - 1] = name;
deferredName = name;
return this;
}
@Override public JsonWriter value(String value) throws IOException {
return add(value);
}
@Override public JsonWriter nullValue() throws IOException {
return add(null);
}
@Override public JsonWriter value(boolean value) throws IOException {
return add(value);
}
@Override public JsonWriter value(Boolean value) throws IOException {
return add(value);
}
@Override public JsonWriter value(double value) throws IOException {
return value(Double.valueOf(value));
}
@Override public JsonWriter value(long value) throws IOException {
return add(value);
}
@Override public JsonWriter value(Number value) throws IOException {
if (!lenient) {
double d = value.doubleValue();
if (d == Double.POSITIVE_INFINITY || d == Double.NEGATIVE_INFINITY || Double.isNaN(d)) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
}
}
return add(value);
}
@Override void promoteNameToValue() throws IOException {
throw new UnsupportedOperationException();
}
@Override public String getPath() {
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
}
@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.");
}
}
/**
* Returns the scope on the top of the stack.
*/
private int peek() {
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
return scopes[stackSize - 1];
}
private ObjectJsonWriter add(Object newTop) {
int scope = peek();
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 {
throw new IllegalStateException("Nesting problem.");
}
return this;
}
}

View File

@@ -286,7 +286,13 @@ final class StandardJsonAdapters {
Map<String, Object> map = new LinkedHashTreeMap<>();
reader.beginObject();
while (reader.hasNext()) {
map.put(reader.nextName(), fromJson(reader));
String name = reader.nextName();
Object value = fromJson(reader);
Object replaced = map.put(name, value);
if (replaced != null) {
throw new JsonDataException("Map key '" + name + "' has multiple values at path "
+ reader.getPath() + ": " + replaced + " and " + value);
}
}
reader.endObject();
return map;

View File

@@ -16,468 +16,15 @@
package com.squareup.moshi;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import okio.Buffer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
@RunWith(Parameterized.class)
public final class BufferedSinkJsonWriterTest {
@Parameter public JsonWriterFactory factory;
@Parameters(name = "{0}")
public static List<Object[]> parameters() {
return JsonWriterFactory.factories();
}
@Test public void nullsValuesNotSerializedByDefault() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
writer.name("a");
writer.nullValue();
writer.endObject();
writer.close();
assertThat(factory.json()).isEqualTo("{}");
}
@Test public void nullsValuesSerializedWhenConfigured() throws IOException {
JsonWriter writer = factory.newWriter();
writer.setSerializeNulls(true);
writer.beginObject();
writer.name("a");
writer.nullValue();
writer.endObject();
writer.close();
assertThat(factory.json()).isEqualTo("{\"a\":null}");
}
@Test public void topLevelBoolean() throws IOException {
JsonWriter writer = factory.newWriter();
writer.value(true);
writer.close();
assertThat(factory.json()).isEqualTo("true");
}
@Test public void topLevelNull() throws IOException {
JsonWriter writer = factory.newWriter();
writer.nullValue();
writer.close();
assertThat(factory.json()).isEqualTo("null");
}
@Test public void topLevelInt() throws IOException {
JsonWriter writer = factory.newWriter();
writer.value(123);
writer.close();
assertThat(factory.json()).isEqualTo("123");
}
@Test public void topLevelDouble() throws IOException {
JsonWriter writer = factory.newWriter();
writer.value(123.4);
writer.close();
assertThat(factory.json()).isEqualTo("123.4");
}
@Test public void topLevelString() throws IOException {
JsonWriter writer = factory.newWriter();
writer.value("a");
writer.close();
assertThat(factory.json()).isEqualTo("\"a\"");
}
@Test public void invalidTopLevelTypes() throws IOException {
JsonWriter writer = factory.newWriter();
writer.name("hello");
try {
writer.value("world");
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void twoNames() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
writer.name("a");
try {
writer.name("a");
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void nameWithoutValue() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
writer.name("a");
try {
writer.endObject();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void valueWithoutName() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
try {
writer.value(true);
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void multipleTopLevelValues() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray().endArray();
try {
writer.beginArray();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void badNestingObject() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.beginObject();
try {
writer.endArray();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void badNestingArray() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.beginArray();
try {
writer.endObject();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void nullName() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
try {
writer.name(null);
fail();
} catch (NullPointerException expected) {
}
}
@Test public void nullStringValue() throws IOException {
JsonWriter writer = factory.newWriter();
writer.setSerializeNulls(true);
writer.beginObject();
writer.name("a");
writer.value((String) null);
writer.endObject();
assertThat(factory.json()).isEqualTo("{\"a\":null}");
}
@Test public void nonFiniteDoubles() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
try {
writer.value(Double.NaN);
fail();
} catch (IllegalArgumentException expected) {
}
try {
writer.value(Double.NEGATIVE_INFINITY);
fail();
} catch (IllegalArgumentException expected) {
}
try {
writer.value(Double.POSITIVE_INFINITY);
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void nonFiniteBoxedDoubles() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
try {
writer.value(new Double(Double.NaN));
fail();
} catch (IllegalArgumentException expected) {
}
try {
writer.value(new Double(Double.NEGATIVE_INFINITY));
fail();
} catch (IllegalArgumentException expected) {
}
try {
writer.value(new Double(Double.POSITIVE_INFINITY));
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void doubles() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value(-0.0);
writer.value(1.0);
writer.value(Double.MAX_VALUE);
writer.value(Double.MIN_VALUE);
writer.value(0.0);
writer.value(-0.5);
writer.value(2.2250738585072014E-308);
writer.value(Math.PI);
writer.value(Math.E);
writer.endArray();
writer.close();
assertThat(factory.json()).isEqualTo("[-0.0,"
+ "1.0,"
+ "1.7976931348623157E308,"
+ "4.9E-324,"
+ "0.0,"
+ "-0.5,"
+ "2.2250738585072014E-308,"
+ "3.141592653589793,"
+ "2.718281828459045]");
}
@Test public void longs() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value(0);
writer.value(1);
writer.value(-1);
writer.value(Long.MIN_VALUE);
writer.value(Long.MAX_VALUE);
writer.endArray();
writer.close();
assertThat(factory.json()).isEqualTo("[0,"
+ "1,"
+ "-1,"
+ "-9223372036854775808,"
+ "9223372036854775807]");
}
@Test public void numbers() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value(new BigInteger("0"));
writer.value(new BigInteger("9223372036854775808"));
writer.value(new BigInteger("-9223372036854775809"));
writer.value(new BigDecimal("3.141592653589793238462643383"));
writer.endArray();
writer.close();
assertThat(factory.json()).isEqualTo("[0,"
+ "9223372036854775808,"
+ "-9223372036854775809,"
+ "3.141592653589793238462643383]");
}
@Test public void booleans() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value(true);
writer.value(false);
writer.endArray();
assertThat(factory.json()).isEqualTo("[true,false]");
}
@Test public void boxedBooleans() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value((Boolean) true);
writer.value((Boolean) false);
writer.value((Boolean) null);
writer.endArray();
assertThat(factory.json()).isEqualTo("[true,false,null]");
}
@Test public void nulls() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.nullValue();
writer.endArray();
assertThat(factory.json()).isEqualTo("[null]");
}
@Test public void strings() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value("a");
writer.value("a\"");
writer.value("\"");
writer.value(":");
writer.value(",");
writer.value("\b");
writer.value("\f");
writer.value("\n");
writer.value("\r");
writer.value("\t");
writer.value(" ");
writer.value("\\");
writer.value("{");
writer.value("}");
writer.value("[");
writer.value("]");
writer.value("\0");
writer.value("\u0019");
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"a\","
+ "\"a\\\"\","
+ "\"\\\"\","
+ "\":\","
+ "\",\","
+ "\"\\b\","
+ "\"\\f\","
+ "\"\\n\","
+ "\"\\r\","
+ "\"\\t\","
+ "\" \","
+ "\"\\\\\","
+ "\"{\","
+ "\"}\","
+ "\"[\","
+ "\"]\","
+ "\"\\u0000\","
+ "\"\\u0019\"]");
}
@Test public void unicodeLineBreaksEscaped() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value("\u2028 \u2029");
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"\\u2028 \\u2029\"]");
}
@Test public void emptyArray() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
assertThat(factory.json()).isEqualTo("[]");
}
@Test public void emptyObject() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
writer.endObject();
assertThat(factory.json()).isEqualTo("{}");
}
@Test public void objectsInArrays() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.beginObject();
writer.name("a").value(5);
writer.name("b").value(false);
writer.endObject();
writer.beginObject();
writer.name("c").value(6);
writer.name("d").value(true);
writer.endObject();
writer.endArray();
assertThat(factory.json()).isEqualTo("[{\"a\":5,\"b\":false},"
+ "{\"c\":6,\"d\":true}]");
}
@Test public void arraysInObjects() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
writer.name("a");
writer.beginArray();
writer.value(5);
writer.value(false);
writer.endArray();
writer.name("b");
writer.beginArray();
writer.value(6);
writer.value(true);
writer.endArray();
writer.endObject();
assertThat(factory.json()).isEqualTo("{\"a\":[5,false],"
+ "\"b\":[6,true]}");
}
@Test public void deepNestingArrays() throws IOException {
JsonWriter writer = factory.newWriter();
for (int i = 0; i < 31; i++) {
writer.beginArray();
}
for (int i = 0; i < 31; i++) {
writer.endArray();
}
assertThat(factory.json())
.isEqualTo("[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]");
}
@Test public void tooDeepNestingArrays() throws IOException {
JsonWriter writer = factory.newWriter();
for (int i = 0; i < 31; i++) {
writer.beginArray();
}
try {
writer.beginArray();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Nesting too deep at $[0][0][0][0][0][0][0][0][0][0][0][0][0]"
+ "[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]: circular reference?");
}
}
@Test public void deepNestingObjects() throws IOException {
JsonWriter writer = factory.newWriter();
for (int i = 0; i < 31; i++) {
writer.beginObject();
writer.name("a");
}
writer.value(true);
for (int i = 0; i < 31; i++) {
writer.endObject();
}
assertThat(factory.json()).isEqualTo(""
+ "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":"
+ "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":"
+ "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":true}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}");
}
@Test public void tooDeepNestingObjects() throws IOException {
JsonWriter writer = factory.newWriter();
for (int i = 0; i < 31; i++) {
writer.beginObject();
writer.name("a");
}
try {
writer.beginObject();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Nesting too deep at $.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a."
+ "a.a.a.a.a.a.a.a.a.a.a.a: circular reference?");
}
}
@Test public void repeatedName() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
writer.name("a").value(true);
writer.name("a").value(false);
writer.endObject();
// JsonWriter doesn't attempt to detect duplicate names
assertThat(factory.json()).isEqualTo("{\"a\":true,\"a\":false}");
}
@Test public void prettyPrintObject() throws IOException {
JsonWriter writer = factory.newWriter();
Buffer buffer = new Buffer();
JsonWriter writer = JsonWriter.of(buffer);
writer.setSerializeNulls(true);
writer.setIndent(" ");
@@ -510,11 +57,12 @@ public final class BufferedSinkJsonWriterTest {
+ " \"i\": 9.0\n"
+ " }\n"
+ "}";
assertThat(factory.json()).isEqualTo(expected);
assertThat(buffer.readUtf8()).isEqualTo(expected);
}
@Test public void prettyPrintArray() throws IOException {
JsonWriter writer = factory.newWriter();
Buffer buffer = new Buffer();
JsonWriter writer = JsonWriter.of(buffer);
writer.setIndent(" ");
writer.beginArray();
@@ -546,99 +94,17 @@ public final class BufferedSinkJsonWriterTest {
+ " 9.0\n"
+ " ]\n"
+ "]";
assertThat(factory.json()).isEqualTo(expected);
assertThat(buffer.readUtf8()).isEqualTo(expected);
}
@Test public void lenientWriterPermitsMultipleTopLevelValues() throws IOException {
JsonWriter writer = factory.newWriter();
writer.setLenient(true);
writer.beginArray();
writer.endArray();
writer.beginArray();
writer.endArray();
writer.close();
assertThat(factory.json()).isEqualTo("[][]");
}
@Test public void strictWriterDoesNotPermitMultipleTopLevelValues() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
try {
writer.beginArray();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void closedWriterThrowsOnStructure() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
writer.close();
try {
writer.beginArray();
fail();
} catch (IllegalStateException expected) {
}
try {
writer.endArray();
fail();
} catch (IllegalStateException expected) {
}
try {
writer.beginObject();
fail();
} catch (IllegalStateException expected) {
}
try {
writer.endObject();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void closedWriterThrowsOnName() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
writer.close();
try {
writer.name("a");
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void closedWriterThrowsOnValue() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
writer.close();
try {
writer.value("a");
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void closedWriterThrowsOnFlush() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
writer.close();
try {
writer.flush();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void writerCloseIsIdempotent() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
writer.close();
writer.close();
@Test public void repeatedNameIgnored() throws IOException {
Buffer buffer = new Buffer();
JsonWriter writer = JsonWriter.of(buffer);
writer.beginObject();
writer.name("a").value(1);
writer.name("a").value(2);
writer.endObject();
// JsonWriter doesn't attempt to detect duplicate names
assertThat(buffer.readUtf8()).isEqualTo("{\"a\":1,\"a\":2}");
}
}

View File

@@ -28,7 +28,7 @@ abstract class JsonReaderFactory {
return JsonReader.of(buffer);
}
@Override public boolean supportsMultipleTopLevelValuesInOneDocument() {
@Override boolean supportsMultipleTopLevelValuesInOneDocument() {
return true;
}
@@ -45,7 +45,7 @@ abstract class JsonReaderFactory {
}
// TODO(jwilson): fix precision checks and delete his method.
@Override public boolean implementsStrictPrecision() {
@Override boolean implementsStrictPrecision() {
return false;
}
@@ -61,11 +61,11 @@ abstract class JsonReaderFactory {
abstract JsonReader newReader(String json) throws IOException;
public boolean supportsMultipleTopLevelValuesInOneDocument() {
boolean supportsMultipleTopLevelValuesInOneDocument() {
return false;
}
public boolean implementsStrictPrecision() {
boolean implementsStrictPrecision() {
return true;
}
}

View File

@@ -15,11 +15,15 @@
*/
package com.squareup.moshi;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import okio.Buffer;
abstract class JsonWriterFactory {
private static final Moshi MOSHI = new Moshi.Builder().build();
private static final JsonAdapter<Object> OBJECT_ADAPTER = MOSHI.adapter(Object.class);
static List<Object[]> factories() {
final JsonWriterFactory bufferedSink = new JsonWriterFactory() {
Buffer buffer;
@@ -35,15 +39,59 @@ abstract class JsonWriterFactory {
return result;
}
@Override boolean supportsMultipleTopLevelValuesInOneDocument() {
return true;
}
@Override public String toString() {
return "BufferedSinkJsonWriter";
}
};
return Arrays.<Object[]>asList(
new Object[] { bufferedSink });
final JsonWriterFactory object = new JsonWriterFactory() {
ObjectJsonWriter writer;
@Override JsonWriter newWriter() {
writer = new ObjectJsonWriter();
return writer;
}
@Override String json() {
// This writer writes a DOM. Use other Moshi features to serialize it as a string.
try {
Buffer buffer = new Buffer();
JsonWriter bufferedSinkWriter = JsonWriter.of(buffer);
bufferedSinkWriter.setSerializeNulls(true);
OBJECT_ADAPTER.toJson(bufferedSinkWriter, writer.root());
return buffer.readUtf8();
} catch (IOException e) {
throw new AssertionError();
}
}
// TODO(jwilson): support BigDecimal and BigInteger and delete his method.
@Override boolean supportsBigNumbers() {
return false;
}
@Override public String toString() {
return "ObjectJsonWriter";
}
};
return Arrays.asList(
new Object[] { bufferedSink },
new Object[] { object });
}
abstract JsonWriter newWriter();
abstract String json();
boolean supportsMultipleTopLevelValuesInOneDocument() {
return false;
}
boolean supportsBigNumbers() {
return true;
}
}

View File

@@ -0,0 +1,565 @@
/*
* Copyright (C) 2010 Google 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
*
* http://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 java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
public final class JsonWriterTest {
@Parameter public JsonWriterFactory factory;
@Parameters(name = "{0}")
public static List<Object[]> parameters() {
return JsonWriterFactory.factories();
}
@Test public void nullsValuesNotSerializedByDefault() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
writer.name("a");
writer.nullValue();
writer.endObject();
writer.close();
assertThat(factory.json()).isEqualTo("{}");
}
@Test public void nullsValuesSerializedWhenConfigured() throws IOException {
JsonWriter writer = factory.newWriter();
writer.setSerializeNulls(true);
writer.beginObject();
writer.name("a");
writer.nullValue();
writer.endObject();
writer.close();
assertThat(factory.json()).isEqualTo("{\"a\":null}");
}
@Test public void topLevelBoolean() throws IOException {
JsonWriter writer = factory.newWriter();
writer.value(true);
writer.close();
assertThat(factory.json()).isEqualTo("true");
}
@Test public void topLevelNull() throws IOException {
JsonWriter writer = factory.newWriter();
writer.nullValue();
writer.close();
assertThat(factory.json()).isEqualTo("null");
}
@Test public void topLevelInt() throws IOException {
JsonWriter writer = factory.newWriter();
writer.value(123);
writer.close();
assertThat(factory.json()).isEqualTo("123");
}
@Test public void topLevelDouble() throws IOException {
JsonWriter writer = factory.newWriter();
writer.value(123.4);
writer.close();
assertThat(factory.json()).isEqualTo("123.4");
}
@Test public void topLevelString() throws IOException {
JsonWriter writer = factory.newWriter();
writer.value("a");
writer.close();
assertThat(factory.json()).isEqualTo("\"a\"");
}
@Test public void invalidTopLevelTypes() throws IOException {
JsonWriter writer = factory.newWriter();
try {
writer.name("hello").value("world");
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void twoNames() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
writer.name("a");
try {
writer.name("a");
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void nameWithoutValue() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
writer.name("a");
try {
writer.endObject();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void valueWithoutName() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
try {
writer.value(true);
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void multipleTopLevelValues() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray().endArray();
try {
writer.beginArray();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void badNestingObject() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.beginObject();
try {
writer.endArray();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void badNestingArray() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.beginArray();
try {
writer.endObject();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void nullName() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
try {
writer.name(null);
fail();
} catch (NullPointerException expected) {
}
}
@Test public void nullStringValue() throws IOException {
JsonWriter writer = factory.newWriter();
writer.setSerializeNulls(true);
writer.beginObject();
writer.name("a");
writer.value((String) null);
writer.endObject();
assertThat(factory.json()).isEqualTo("{\"a\":null}");
}
@Test public void nonFiniteDoubles() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
try {
writer.value(Double.NaN);
fail();
} catch (IllegalArgumentException expected) {
}
try {
writer.value(Double.NEGATIVE_INFINITY);
fail();
} catch (IllegalArgumentException expected) {
}
try {
writer.value(Double.POSITIVE_INFINITY);
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void nonFiniteBoxedDoubles() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
try {
writer.value(new Double(Double.NaN));
fail();
} catch (IllegalArgumentException expected) {
}
try {
writer.value(new Double(Double.NEGATIVE_INFINITY));
fail();
} catch (IllegalArgumentException expected) {
}
try {
writer.value(new Double(Double.POSITIVE_INFINITY));
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void doubles() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value(-0.0);
writer.value(1.0);
writer.value(Double.MAX_VALUE);
writer.value(Double.MIN_VALUE);
writer.value(0.0);
writer.value(-0.5);
writer.value(2.2250738585072014E-308);
writer.value(Math.PI);
writer.value(Math.E);
writer.endArray();
writer.close();
assertThat(factory.json()).isEqualTo("[-0.0,"
+ "1.0,"
+ "1.7976931348623157E308,"
+ "4.9E-324,"
+ "0.0,"
+ "-0.5,"
+ "2.2250738585072014E-308,"
+ "3.141592653589793,"
+ "2.718281828459045]");
}
@Test public void longs() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value(0);
writer.value(1);
writer.value(-1);
writer.value(Long.MIN_VALUE);
writer.value(Long.MAX_VALUE);
writer.endArray();
writer.close();
assertThat(factory.json()).isEqualTo("[0,"
+ "1,"
+ "-1,"
+ "-9223372036854775808,"
+ "9223372036854775807]");
}
@Test public void numbers() throws IOException {
assumeTrue(factory.supportsBigNumbers());
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value(new BigInteger("0"));
writer.value(new BigInteger("9223372036854775808"));
writer.value(new BigInteger("-9223372036854775809"));
writer.value(new BigDecimal("3.141592653589793238462643383"));
writer.endArray();
writer.close();
assertThat(factory.json()).isEqualTo("[0,"
+ "9223372036854775808,"
+ "-9223372036854775809,"
+ "3.141592653589793238462643383]");
}
@Test public void booleans() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value(true);
writer.value(false);
writer.endArray();
assertThat(factory.json()).isEqualTo("[true,false]");
}
@Test public void boxedBooleans() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value((Boolean) true);
writer.value((Boolean) false);
writer.value((Boolean) null);
writer.endArray();
assertThat(factory.json()).isEqualTo("[true,false,null]");
}
@Test public void nulls() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.nullValue();
writer.endArray();
assertThat(factory.json()).isEqualTo("[null]");
}
@Test public void strings() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value("a");
writer.value("a\"");
writer.value("\"");
writer.value(":");
writer.value(",");
writer.value("\b");
writer.value("\f");
writer.value("\n");
writer.value("\r");
writer.value("\t");
writer.value(" ");
writer.value("\\");
writer.value("{");
writer.value("}");
writer.value("[");
writer.value("]");
writer.value("\0");
writer.value("\u0019");
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"a\","
+ "\"a\\\"\","
+ "\"\\\"\","
+ "\":\","
+ "\",\","
+ "\"\\b\","
+ "\"\\f\","
+ "\"\\n\","
+ "\"\\r\","
+ "\"\\t\","
+ "\" \","
+ "\"\\\\\","
+ "\"{\","
+ "\"}\","
+ "\"[\","
+ "\"]\","
+ "\"\\u0000\","
+ "\"\\u0019\"]");
}
@Test public void unicodeLineBreaksEscaped() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.value("\u2028 \u2029");
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"\\u2028 \\u2029\"]");
}
@Test public void emptyArray() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
assertThat(factory.json()).isEqualTo("[]");
}
@Test public void emptyObject() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
writer.endObject();
assertThat(factory.json()).isEqualTo("{}");
}
@Test public void objectsInArrays() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.beginObject();
writer.name("a").value(5);
writer.name("b").value(false);
writer.endObject();
writer.beginObject();
writer.name("c").value(6);
writer.name("d").value(true);
writer.endObject();
writer.endArray();
assertThat(factory.json()).isEqualTo("[{\"a\":5,\"b\":false},"
+ "{\"c\":6,\"d\":true}]");
}
@Test public void arraysInObjects() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginObject();
writer.name("a");
writer.beginArray();
writer.value(5);
writer.value(false);
writer.endArray();
writer.name("b");
writer.beginArray();
writer.value(6);
writer.value(true);
writer.endArray();
writer.endObject();
assertThat(factory.json()).isEqualTo("{\"a\":[5,false],"
+ "\"b\":[6,true]}");
}
@Test public void deepNestingArrays() throws IOException {
JsonWriter writer = factory.newWriter();
for (int i = 0; i < 31; i++) {
writer.beginArray();
}
for (int i = 0; i < 31; i++) {
writer.endArray();
}
assertThat(factory.json())
.isEqualTo("[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]");
}
@Test public void tooDeepNestingArrays() throws IOException {
JsonWriter writer = factory.newWriter();
for (int i = 0; i < 31; i++) {
writer.beginArray();
}
try {
writer.beginArray();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Nesting too deep at $[0][0][0][0][0][0][0][0][0][0][0][0][0]"
+ "[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]: circular reference?");
}
}
@Test public void deepNestingObjects() throws IOException {
JsonWriter writer = factory.newWriter();
for (int i = 0; i < 31; i++) {
writer.beginObject();
writer.name("a");
}
writer.value(true);
for (int i = 0; i < 31; i++) {
writer.endObject();
}
assertThat(factory.json()).isEqualTo(""
+ "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":"
+ "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":"
+ "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":true}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}");
}
@Test public void tooDeepNestingObjects() throws IOException {
JsonWriter writer = factory.newWriter();
for (int i = 0; i < 31; i++) {
writer.beginObject();
writer.name("a");
}
try {
writer.beginObject();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Nesting too deep at $.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a."
+ "a.a.a.a.a.a.a.a.a.a.a.a: circular reference?");
}
}
@Test public void lenientWriterPermitsMultipleTopLevelValues() throws IOException {
assumeTrue(factory.supportsMultipleTopLevelValuesInOneDocument());
JsonWriter writer = factory.newWriter();
writer.setLenient(true);
writer.beginArray();
writer.endArray();
writer.beginArray();
writer.endArray();
writer.close();
assertThat(factory.json()).isEqualTo("[][]");
}
@Test public void strictWriterDoesNotPermitMultipleTopLevelValues() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
try {
writer.beginArray();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void closedWriterThrowsOnStructure() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
writer.close();
try {
writer.beginArray();
fail();
} catch (IllegalStateException expected) {
}
try {
writer.endArray();
fail();
} catch (IllegalStateException expected) {
}
try {
writer.beginObject();
fail();
} catch (IllegalStateException expected) {
}
try {
writer.endObject();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void closedWriterThrowsOnName() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
writer.close();
try {
writer.name("a");
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void closedWriterThrowsOnValue() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
writer.close();
try {
writer.value("a");
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void closedWriterThrowsOnFlush() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
writer.close();
try {
writer.flush();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void writerCloseIsIdempotent() throws IOException {
JsonWriter writer = factory.newWriter();
writer.beginArray();
writer.endArray();
writer.close();
writer.close();
}
}

View File

@@ -105,7 +105,7 @@ public final class MapJsonAdapterTest {
fromJson(String.class, Integer.class, "{\"c\":1,\"c\":2}");
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Map key 'c' has multiple values at path $.c");
assertThat(expected).hasMessage("Map key 'c' has multiple values at path $.c: 1 and 2");
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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
*
* http://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 java.io.IOException;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.junit.Assert.fail;
public final class ObjectJsonWriterTest {
@SuppressWarnings("unchecked")
@Test public void array() throws Exception {
ObjectJsonWriter writer = new ObjectJsonWriter();
writer.beginArray();
writer.value("s");
writer.value(1.5d);
writer.value(true);
writer.nullValue();
writer.endArray();
assertThat((List<Object>) writer.root()).containsExactly("s", 1.5d, true, null);
}
@Test public void object() throws Exception {
ObjectJsonWriter writer = new ObjectJsonWriter();
writer.setSerializeNulls(true);
writer.beginObject();
writer.name("a").value("s");
writer.name("b").value(1.5d);
writer.name("c").value(true);
writer.name("d").nullValue();
writer.endObject();
assertThat((Map<?, ?>) writer.root()).containsExactly(
entry("a", "s"), entry("b", 1.5d), entry("c", true), entry("d", null));
}
@Test public void repeatedNameThrows() throws IOException {
ObjectJsonWriter writer = new ObjectJsonWriter();
writer.beginObject();
writer.name("a").value(1L);
try {
writer.name("a").value(2L);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage(
"Map key 'a' has multiple values at path $.a: 1 and 2");
}
}
}