diff --git a/moshi/src/main/java/com/squareup/moshi/JsonReader.java b/moshi/src/main/java/com/squareup/moshi/JsonReader.java index bcf1194..d65429e 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonReader.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonReader.java @@ -514,8 +514,19 @@ public abstract class JsonReader implements Closeable { /** * Changes the reader to treat the next name as a string value. This is useful for map adapters so * that arbitrary type adapters can use {@link #nextString} to read a name value. + * + *

In this example, calling this method allows two sequential calls to {@link #nextString()}: + *

 {@code
+   *
+   *     JsonReader reader = JsonReader.of(new Buffer().writeUtf8("{\"a\":\"b\"}"));
+   *     reader.beginObject();
+   *     reader.promoteNameToValue();
+   *     assertEquals("a", reader.nextString());
+   *     assertEquals("b", reader.nextString());
+   *     reader.endObject();
+   * }
*/ - abstract void promoteNameToValue() throws IOException; + public abstract void promoteNameToValue() throws IOException; /** * A set of strings to be chosen with {@link #selectName} or {@link #selectString}. This prepares diff --git a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java index 70e48a8..dfcc10e 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java @@ -1158,7 +1158,7 @@ final class JsonUtf8Reader extends JsonReader { } } - @Override void promoteNameToValue() throws IOException { + @Override public void promoteNameToValue() throws IOException { if (hasNext()) { peekedString = nextName(); peeked = PEEKED_BUFFERED; diff --git a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java index e8d88be..1961039 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java @@ -162,12 +162,13 @@ final class JsonUtf8Writer extends JsonWriter { throw new IllegalStateException("JsonWriter is closed."); } int context = peekScope(); - if ((context != EMPTY_OBJECT && context != NONEMPTY_OBJECT) || deferredName != null) { + if ((context != EMPTY_OBJECT && context != NONEMPTY_OBJECT) + || deferredName != null + || promoteValueToName) { throw new IllegalStateException("Nesting problem."); } deferredName = name; pathNames[stackSize - 1] = name; - promoteValueToName = false; return this; } @@ -184,6 +185,7 @@ final class JsonUtf8Writer extends JsonWriter { return nullValue(); } if (promoteValueToName) { + promoteValueToName = false; return name(value); } writeDeferredName(); @@ -236,6 +238,7 @@ final class JsonUtf8Writer extends JsonWriter { throw new IllegalArgumentException("Numeric values must be finite, but was " + value); } if (promoteValueToName) { + promoteValueToName = false; return name(Double.toString(value)); } writeDeferredName(); @@ -247,6 +250,7 @@ final class JsonUtf8Writer extends JsonWriter { @Override public JsonWriter value(long value) throws IOException { if (promoteValueToName) { + promoteValueToName = false; return name(Long.toString(value)); } writeDeferredName(); @@ -267,6 +271,7 @@ final class JsonUtf8Writer extends JsonWriter { throw new IllegalArgumentException("Numeric values must be finite, but was " + value); } if (promoteValueToName) { + promoteValueToName = false; return name(string); } writeDeferredName(); diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueReader.java b/moshi/src/main/java/com/squareup/moshi/JsonValueReader.java index df1d66b..85ba507 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonValueReader.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonValueReader.java @@ -330,7 +330,7 @@ final class JsonValueReader extends JsonReader { return new JsonValueReader(this); } - @Override void promoteNameToValue() throws IOException { + @Override public void promoteNameToValue() throws IOException { if (hasNext()) { String name = nextName(); push(name); diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java index 21fa06b..2aab894 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java @@ -130,17 +130,17 @@ final class JsonValueWriter extends JsonWriter { if (stackSize == 0) { throw new IllegalStateException("JsonWriter is closed."); } - if (peekScope() != EMPTY_OBJECT || deferredName != null) { + if (peekScope() != EMPTY_OBJECT || deferredName != null || promoteValueToName) { throw new IllegalStateException("Nesting problem."); } deferredName = name; pathNames[stackSize - 1] = name; - promoteValueToName = false; return this; } @Override public JsonWriter value(@Nullable String value) throws IOException { if (promoteValueToName) { + promoteValueToName = false; return name(value); } add(value); @@ -184,6 +184,7 @@ final class JsonValueWriter extends JsonWriter { throw new IllegalArgumentException("Numeric values must be finite, but was " + value); } if (promoteValueToName) { + promoteValueToName = false; return name(Double.toString(value)); } add(value); @@ -193,6 +194,7 @@ final class JsonValueWriter extends JsonWriter { @Override public JsonWriter value(long value) throws IOException { if (promoteValueToName) { + promoteValueToName = false; return name(Long.toString(value)); } add(value); @@ -223,6 +225,7 @@ final class JsonValueWriter extends JsonWriter { ? ((BigDecimal) value) : new BigDecimal(value.toString()); if (promoteValueToName) { + promoteValueToName = false; return name(bigDecimalValue.toString()); } add(bigDecimalValue); diff --git a/moshi/src/main/java/com/squareup/moshi/JsonWriter.java b/moshi/src/main/java/com/squareup/moshi/JsonWriter.java index af39020..1ff1610 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonWriter.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonWriter.java @@ -443,8 +443,20 @@ public abstract class JsonWriter implements Closeable, Flushable { /** * Changes the writer to treat the next value as a string name. This is useful for map adapters so * that arbitrary type adapters can use {@link #value} to write a name value. + * + *

In this example, calling this method allows two sequential calls to {@link #value(String)} + * to produce the object, {@code {"a": "b"}}. + *

 {@code
+   *
+   *     JsonWriter writer = JsonWriter.of(...);
+   *     writer.beginObject();
+   *     writer.promoteValueToName();
+   *     writer.value("a");
+   *     writer.value("b");
+   *     writer.endObject();
+   * }
*/ - final void promoteValueToName() throws IOException { + public final void promoteValueToName() throws IOException { int context = peekScope(); if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) { throw new IllegalStateException("Nesting problem."); diff --git a/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java b/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java index 213ff5f..3ed7dda 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Set; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -1158,6 +1157,85 @@ public final class JsonReaderTest { reader.endObject(); } + @Test public void promoteStringNameToValue() throws IOException { + JsonReader reader = newReader("{\"a\":\"b\"}"); + reader.beginObject(); + reader.promoteNameToValue(); + assertEquals("a", reader.nextString()); + assertEquals("b", reader.nextString()); + reader.endObject(); + } + + @Test public void promoteDoubleNameToValue() throws IOException { + JsonReader reader = newReader("{\"5\":\"b\"}"); + reader.beginObject(); + reader.promoteNameToValue(); + assertEquals(5.0, reader.nextDouble(), 0.0); + assertEquals("b", reader.nextString()); + reader.endObject(); + } + + @Test public void promoteLongNameToValue() throws IOException { + JsonReader reader = newReader("{\"5\":\"b\"}"); + reader.beginObject(); + reader.promoteNameToValue(); + assertEquals(5L, reader.nextLong()); + assertEquals("b", reader.nextString()); + reader.endObject(); + } + + @Test public void promoteNullNameToValue() throws IOException { + JsonReader reader = newReader("{\"null\":\"b\"}"); + reader.beginObject(); + reader.promoteNameToValue(); + try { + reader.nextNull(); + fail(); + } catch (JsonDataException expected) { + } + assertEquals("null", reader.nextString()); + } + + @Test public void promoteBooleanNameToValue() throws IOException { + JsonReader reader = newReader("{\"true\":\"b\"}"); + reader.beginObject(); + reader.promoteNameToValue(); + try { + reader.nextBoolean(); + fail(); + } catch (JsonDataException expected) { + } + assertEquals("true", reader.nextString()); + } + + @Test public void promoteBooleanNameToValueCannotBeReadAsName() throws IOException { + JsonReader reader = newReader("{\"true\":\"b\"}"); + reader.beginObject(); + reader.promoteNameToValue(); + try { + reader.nextName(); + fail(); + } catch (JsonDataException expected) { + } + assertEquals("true", reader.nextString()); + } + + @Test public void promoteSkippedNameToValue() throws IOException { + JsonReader reader = newReader("{\"true\":\"b\"}"); + reader.beginObject(); + reader.promoteNameToValue(); + reader.skipValue(); + assertEquals("b", reader.nextString()); + } + + @Test public void promoteNameToValueAtEndOfObject() throws IOException { + JsonReader reader = newReader("{}"); + reader.beginObject(); + reader.promoteNameToValue(); + assertThat(reader.hasNext()).isFalse(); + reader.endObject(); + } + @Test public void optionsStrings() { String[] options = new String[] { "a", "b", "c" }; JsonReader.Options abc = JsonReader.Options.of("a", "b", "c"); diff --git a/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java b/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java index 692ea51..d2e441b 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java @@ -609,7 +609,7 @@ public final class JsonWriterTest { assertThat(expected).hasMessage("Nesting problem."); } } - + @Test public void danglingNameFails() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); @@ -814,4 +814,88 @@ public final class JsonWriterTest { assertThat(e).hasMessage("Map keys must be non-null"); } } + + @Test public void promoteStringNameToValue() throws IOException { + JsonWriter writer = factory.newWriter(); + writer.beginObject(); + writer.promoteValueToName(); + writer.value("a"); + writer.value("b"); + writer.endObject(); + assertThat(factory.json()).isEqualTo("{\"a\":\"b\"}"); + } + + @Test public void promoteDoubleNameToValue() throws IOException { + JsonWriter writer = factory.newWriter(); + writer.beginObject(); + writer.promoteValueToName(); + writer.value(5.0); + writer.value("b"); + writer.endObject(); + assertThat(factory.json()).isEqualTo("{\"5.0\":\"b\"}"); + } + + @Test public void promoteLongNameToValue() throws IOException { + JsonWriter writer = factory.newWriter(); + writer.beginObject(); + writer.promoteValueToName(); + writer.value(5L); + writer.value("b"); + writer.endObject(); + assertThat(factory.json()).isEqualTo("{\"5\":\"b\"}"); + } + + @Test public void promoteNumberNameToValue() throws IOException { + JsonWriter writer = factory.newWriter(); + writer.beginObject(); + writer.promoteValueToName(); + writer.value(BigInteger.ONE); + writer.value("b"); + writer.endObject(); + assertThat(factory.json()).isEqualTo("{\"1\":\"b\"}"); + } + + @Test public void promoteNullNameToValue() throws IOException { + JsonWriter writer = factory.newWriter(); + writer.beginObject(); + writer.promoteValueToName(); + try { + writer.nullValue(); + fail(); + } catch (IllegalStateException expected) { + assertThat(expected).hasMessage("null cannot be used as a map key in JSON at path $."); + } + } + + @Test public void promoteBooleanNameToValue() throws IOException { + JsonWriter writer = factory.newWriter(); + writer.beginObject(); + writer.promoteValueToName(); + try { + writer.value(true); + fail(); + } catch (IllegalStateException expected) { + assertThat(expected).hasMessage("Boolean cannot be used as a map key in JSON at path $."); + } + } + + @Test public void promoteNameToValueCannotBeWrittenAsName() throws IOException { + JsonWriter writer = factory.newWriter(); + writer.beginObject(); + writer.promoteValueToName(); + try { + writer.name("a"); + fail(); + } catch (IllegalStateException expected) { + assertThat(expected).hasMessage("Nesting problem."); + } + } + + @Test public void promoteNameToValueAtEndOfObject() throws IOException { + JsonWriter writer = factory.newWriter(); + writer.beginObject(); + writer.promoteValueToName(); + writer.endObject(); + assertThat(factory.json()).isEqualTo("{}"); + } }