Merge pull request #1225 from square/jwilson.0914.nextSourceHacks

Small improvements to JsonReader.nextSource
diff --git a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java
index c0f7d2d..06b9652 100644
--- a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java
+++ b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java
@@ -241,10 +241,6 @@
   }
 
   private int doPeek() throws IOException {
-    if (valueSource != null) {
-      valueSource.discard();
-      valueSource = null;
-    }
     int peekStack = scopes[stackSize - 1];
     if (peekStack == JsonScope.EMPTY_ARRAY) {
       scopes[stackSize - 1] = JsonScope.NONEMPTY_ARRAY;
@@ -329,6 +325,13 @@
       } else {
         checkLenient();
       }
+    } else if (peekStack == JsonScope.STREAMING_VALUE) {
+      valueSource.discard();
+      valueSource = null;
+      stackSize--;
+      pathIndices[stackSize - 1]++;
+      pathNames[stackSize - 1] = "null";
+      return doPeek();
     } else if (peekStack == JsonScope.CLOSED) {
       throw new IllegalStateException("JsonReader is closed");
     }
@@ -1067,9 +1070,8 @@
     }
 
     valueSource = new JsonValueSource(source, prefix, state, valueSourceStackSize);
+    pushScope(JsonScope.STREAMING_VALUE);
     peeked = PEEKED_NONE;
-    pathIndices[stackSize - 1]++;
-    pathNames[stackSize - 1] = "null";
 
     return Okio.buffer(valueSource);
   }
diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueSource.java b/moshi/src/main/java/com/squareup/moshi/JsonValueSource.java
index 4bc895a..7f1f5ef 100644
--- a/moshi/src/main/java/com/squareup/moshi/JsonValueSource.java
+++ b/moshi/src/main/java/com/squareup/moshi/JsonValueSource.java
@@ -76,7 +76,14 @@
   }
 
   /**
-   * Advance {@link #limit} until it is at least {@code byteCount} or the JSON object is complete.
+   * Advance {@link #limit} until any of these conditions are met:
+   *
+   * <ul>
+   *   <li>Limit is at least {@code byteCount}. We can satisfy the caller's request!
+   *   <li>The JSON value is complete. This stream is exhausted.
+   *   <li>We have some data to return and returning more would require reloading the buffer. We
+   *       prefer to return some data immediately when more data requires blocking.
+   * </ul>
    *
    * @throws EOFException if the stream is exhausted before the JSON object completes.
    */
@@ -87,9 +94,10 @@
         return;
       }
 
-      // If advancing requires more data in the buffer, grow it.
+      // If we can't return any bytes without more data in the buffer, grow the buffer.
       if (limit == buffer.size()) {
-        source.require(limit + 1L);
+        if (limit > 0L) return;
+        source.require(1L);
       }
 
       // Find the next interesting character for the current state. If the buffer doesn't have one,
@@ -196,6 +204,7 @@
     if (!prefix.exhausted()) {
       long prefixResult = prefix.read(sink, byteCount);
       byteCount -= prefixResult;
+      if (buffer.exhausted()) return prefixResult; // Defer a blocking call.
       long suffixResult = read(sink, byteCount);
       return suffixResult != -1L ? suffixResult + prefixResult : prefixResult;
     }
diff --git a/moshi/src/test/java/com/squareup/moshi/JsonUtf8ReaderTest.java b/moshi/src/test/java/com/squareup/moshi/JsonUtf8ReaderTest.java
index 2b04d4e..6317445 100644
--- a/moshi/src/test/java/com/squareup/moshi/JsonUtf8ReaderTest.java
+++ b/moshi/src/test/java/com/squareup/moshi/JsonUtf8ReaderTest.java
@@ -38,6 +38,7 @@
 import okio.BufferedSource;
 import okio.ForwardingSource;
 import okio.Okio;
+import okio.Source;
 import org.junit.Ignore;
 import org.junit.Test;
 
@@ -1408,4 +1409,81 @@
       assertThat(valueSource.readUtf8()).isEqualTo("-2");
     }
   }
+
+  /**
+   * Confirm that {@link JsonReader#nextSource} doesn't load data from the underlying stream until
+   * its required by the caller. If the source is backed by a slow network stream, we want users to
+   * get data as it arrives.
+   *
+   * <p>Because we don't have a slow stream in this test, we just add bytes to our underlying stream
+   * immediately before they're needed.
+   */
+  @Test
+  public void nextSourceStreams() throws IOException {
+    Buffer stream = new Buffer();
+    stream.writeUtf8("[\"");
+
+    JsonReader reader = JsonReader.of(Okio.buffer((Source) stream));
+    reader.beginArray();
+    BufferedSource source = reader.nextSource();
+    assertThat(source.readUtf8(1)).isEqualTo("\"");
+    stream.writeUtf8("hello");
+    assertThat(source.readUtf8(5)).isEqualTo("hello");
+    stream.writeUtf8("world");
+    assertThat(source.readUtf8(5)).isEqualTo("world");
+    stream.writeUtf8("\"");
+    assertThat(source.readUtf8(1)).isEqualTo("\"");
+    stream.writeUtf8("]");
+    assertThat(source.exhausted()).isTrue();
+    reader.endArray();
+  }
+
+  @Test
+  public void nextSourceObjectAfterSelect() throws IOException {
+    // language=JSON
+    JsonReader reader = newReader("[\"p\u0065psi\"]");
+    reader.beginArray();
+    assertThat(reader.selectName(JsonReader.Options.of("coke"))).isEqualTo(-1);
+    try (BufferedSource valueSource = reader.nextSource()) {
+      assertThat(valueSource.readUtf8()).isEqualTo("\"pepsi\""); // not the original characters!
+    }
+  }
+
+  @Test
+  public void nextSourceObjectAfterPromoteNameToValue() throws IOException {
+    // language=JSON
+    JsonReader reader = newReader("{\"a\":true}");
+    reader.beginObject();
+    reader.promoteNameToValue();
+    try (BufferedSource valueSource = reader.nextSource()) {
+      assertThat(valueSource.readUtf8()).isEqualTo("\"a\"");
+    }
+    assertThat(reader.nextBoolean()).isEqualTo(true);
+    reader.endObject();
+  }
+
+  @Test
+  public void nextSourcePath() throws IOException {
+    // language=JSON
+    JsonReader reader = newReader("{\"a\":true,\"b\":[],\"c\":false}");
+    reader.beginObject();
+
+    assertThat(reader.nextName()).isEqualTo("a");
+    assertThat(reader.getPath()).isEqualTo("$.a");
+    assertThat(reader.nextBoolean()).isTrue();
+    assertThat(reader.getPath()).isEqualTo("$.a");
+
+    assertThat(reader.nextName()).isEqualTo("b");
+    try (BufferedSource valueSource = reader.nextSource()) {
+      assertThat(reader.getPath()).isEqualTo("$.b");
+      assertThat(valueSource.readUtf8()).isEqualTo("[]");
+    }
+    assertThat(reader.getPath()).isEqualTo("$.b");
+
+    assertThat(reader.nextName()).isEqualTo("c");
+    assertThat(reader.getPath()).isEqualTo("$.c");
+    assertThat(reader.nextBoolean()).isFalse();
+    assertThat(reader.getPath()).isEqualTo("$.c");
+    reader.endObject();
+  }
 }