diff --git a/moshi/src/main/java/com/squareup/moshi/ObjectJsonReader.java b/moshi/src/main/java/com/squareup/moshi/ObjectJsonReader.java new file mode 100644 index 0000000..3f61ede --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/ObjectJsonReader.java @@ -0,0 +1,290 @@ +/* + * 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.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +/** + * This class reads a JSON document by traversing a Java object comprising maps, lists, and JSON + * primitives. It does depth-first traversal keeping a stack starting with the root object. During + * traversal a stack tracks the current position in the document: + * + * + */ +final class ObjectJsonReader extends JsonReader { + /** Sentinel object pushed on {@link #stack} when the reader is closed. */ + private static final Object JSON_READER_CLOSED = new Object(); + + private int stackSize = 0; + 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 boolean lenient; + private boolean failOnUnknown; + + public ObjectJsonReader(Object root) { + scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT; + stack[stackSize++] = root; + } + + @Override public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + @Override public boolean isLenient() { + return lenient; + } + + @Override public void setFailOnUnknown(boolean failOnUnknown) { + this.failOnUnknown = failOnUnknown; + } + + @Override public boolean failOnUnknown() { + return failOnUnknown; + } + + @Override public void beginArray() throws IOException { + List peeked = require(List.class, Token.BEGIN_ARRAY); + + ListIterator iterator = peeked.listIterator(); + stack[stackSize - 1] = iterator; + scopes[stackSize - 1] = JsonScope.EMPTY_ARRAY; + pathIndices[stackSize - 1] = 0; + + // If the iterator isn't empty push its first value onto the stack. + if (iterator.hasNext()) { + stack[stackSize] = iterator.next(); + stackSize++; + } + } + + @Override public void endArray() throws IOException { + ListIterator peeked = require(ListIterator.class, Token.END_ARRAY); + if (peeked.hasNext()) { + throw new JsonDataException( + "Expected " + Token.END_ARRAY + " but was " + peek() + " at path " + getPath()); + } + remove(); + } + + @Override public void beginObject() throws IOException { + Map peeked = require(Map.class, Token.BEGIN_OBJECT); + + Iterator iterator = peeked.entrySet().iterator(); + stack[stackSize - 1] = iterator; + scopes[stackSize - 1] = JsonScope.EMPTY_OBJECT; + + // If the iterator isn't empty push its first value onto the stack. + if (iterator.hasNext()) { + stack[stackSize] = iterator.next(); + stackSize++; + } + } + + @Override public void endObject() throws IOException { + Iterator peeked = require(Iterator.class, Token.END_OBJECT); + if (peeked instanceof ListIterator || peeked.hasNext()) { + throw new JsonDataException( + "Expected " + Token.END_OBJECT + " but was " + peek() + " at path " + getPath()); + } + pathNames[stackSize - 1] = null; + remove(); + } + + @Override public boolean hasNext() throws IOException { + // TODO(jwilson): this is consistent with BufferedSourceJsonReader but it doesn't make sense. + if (stackSize == 0) return true; + + Object peeked = stack[stackSize - 1]; + return !(peeked instanceof Iterator) || ((Iterator) peeked).hasNext(); + } + + @Override public Token peek() throws IOException { + if (stackSize == 0) return Token.END_DOCUMENT; + + // If the top of the stack is an iterator, take its first element and push it on the stack. + Object peeked = stack[stackSize - 1]; + if (peeked instanceof ListIterator) return Token.END_ARRAY; + if (peeked instanceof Iterator) return Token.END_OBJECT; + if (peeked instanceof List) return Token.BEGIN_ARRAY; + if (peeked instanceof Map) return Token.BEGIN_OBJECT; + if (peeked instanceof Map.Entry) return Token.NAME; + if (peeked instanceof String) return Token.STRING; + if (peeked instanceof Boolean) return Token.BOOLEAN; + if (peeked instanceof Number) return Token.NUMBER; + if (peeked == null) return Token.NULL; + if (peeked == JSON_READER_CLOSED) throw new IllegalStateException("JsonReader is closed"); + + throw new JsonDataException("Expected a JSON value but was a " + peeked.getClass().getName() + + " at path " + getPath()); + } + + @Override public String nextName() throws IOException { + Object peeked = require(Map.Entry.class, Token.NAME); + + // Swap the Map.Entry for its value on the stack and return its key. + String result = (String) ((Map.Entry) peeked).getKey(); + stack[stackSize - 1] = ((Map.Entry) peeked).getValue(); + pathNames[stackSize - 2] = result; + return result; + } + + @Override int selectName(Options options) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override public String nextString() throws IOException { + String peeked = require(String.class, Token.STRING); + remove(); + return peeked; + } + + @Override int selectString(Options options) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override public boolean nextBoolean() throws IOException { + Boolean peeked = require(Boolean.class, Token.BOOLEAN); + remove(); + return peeked; + } + + @Override public T nextNull() throws IOException { + require(Void.class, Token.NULL); + remove(); + return null; + } + + @Override public double nextDouble() throws IOException { + Number peeked = require(Number.class, Token.NUMBER); + remove(); + return peeked.doubleValue(); // TODO(jwilson): precision check? + } + + @Override public long nextLong() throws IOException { + Number peeked = require(Number.class, Token.NUMBER); + remove(); + return peeked.longValue(); // TODO(jwilson): precision check? + } + + @Override public int nextInt() throws IOException { + Number peeked = require(Number.class, Token.NUMBER); + remove(); + return peeked.intValue(); // TODO(jwilson): precision check? + } + + @Override public void skipValue() throws IOException { + if (failOnUnknown) { + throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath()); + } + + // If this element is in an object clear out the key. + if (stackSize > 1) { + pathNames[stackSize - 2] = "null"; + } + + Object skipped = stackSize != 0 ? stack[stackSize - 1] : null; + + if (skipped instanceof Map.Entry) { + // We're skipping a name. Promote the map entry's value. + Map.Entry entry = (Map.Entry) stack[stackSize - 1]; + stack[stackSize - 1] = entry.getValue(); + } else if (stackSize > 0) { + // We're skipping a value. + remove(); + } + } + + @Override public String getPath() { + return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices); + } + + @Override void promoteNameToValue() throws IOException { + Object peeked = require(Map.Entry.class, Token.NAME); + + stackSize++; + stack[stackSize - 2] = ((Map.Entry) peeked).getValue(); + stack[stackSize - 1] = ((Map.Entry) peeked).getKey(); + } + + @Override public void close() throws IOException { + Arrays.fill(stack, 0, stackSize, null); + stack[0] = JSON_READER_CLOSED; + scopes[0] = JsonScope.CLOSED; + stackSize = 1; + } + + /** + * Returns the top of the stack which is required to be a {@code type}. Throws if this reader is + * closed, or if the type isn't what was expected. + */ + private T require(Class type, Token expected) throws IOException { + Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null); + + if (type.isInstance(peeked)) { + return type.cast(peeked); + } + if (peeked == null && expected == Token.NULL) { + return null; + } + if (peeked == JSON_READER_CLOSED) { + throw new IllegalStateException("JsonReader is closed"); + } + throw new JsonDataException( + "Expected " + expected + " but was " + peek() + " at path " + getPath()); + } + + /** + * Removes a value and prepares for the next. If we're iterating a map or list this advances the + * iterator. + */ + private void remove() { + stackSize--; + stack[stackSize] = null; + scopes[stackSize] = 0; + + // If we're iterating an array or an object push its next element on to the stack. + if (stackSize > 0) { + pathIndices[stackSize - 1]++; + + Object parent = stack[stackSize - 1]; + if (parent instanceof Iterator && ((Iterator) parent).hasNext()) { + stack[stackSize] = ((Iterator) parent).next(); + stackSize++; + } + } + } +} diff --git a/moshi/src/test/java/com/squareup/moshi/JsonReaderPathTest.java b/moshi/src/test/java/com/squareup/moshi/JsonReaderPathTest.java index 47c4e36..465460e 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonReaderPathTest.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonReaderPathTest.java @@ -16,14 +16,50 @@ package com.squareup.moshi; import java.io.IOException; +import java.util.Arrays; +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 com.squareup.moshi.TestUtil.newReader; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assume.assumeTrue; +@RunWith(Parameterized.class) public final class JsonReaderPathTest { + interface Factory { + Factory BUFFERED_SOURCE = new Factory() { + @Override public JsonReader newReader(String json) { + Buffer buffer = new Buffer().writeUtf8(json); + return JsonReader.of(buffer); + } + }; + + Factory JSON_OBJECT = new Factory() { + @Override public JsonReader newReader(String json) throws IOException { + Moshi moshi = new Moshi.Builder().build(); + Object object = moshi.adapter(Object.class).fromJson(json); + return new ObjectJsonReader(object); + } + }; + + JsonReader newReader(String json) throws IOException; + } + + @Parameters(name = "{0}") + public static List parameters() { + return Arrays.asList( + new Object[] { Factory.BUFFERED_SOURCE}, + new Object[] { Factory.JSON_OBJECT}); + } + + @Parameter public Factory factory; + @Test public void path() throws IOException { - JsonReader reader = newReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}"); + JsonReader reader = factory.newReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}"); assertThat(reader.getPath()).isEqualTo("$"); reader.beginObject(); assertThat(reader.getPath()).isEqualTo("$."); @@ -62,7 +98,7 @@ public final class JsonReaderPathTest { } @Test public void arrayOfObjects() throws IOException { - JsonReader reader = newReader("[{},{},{}]"); + JsonReader reader = factory.newReader("[{},{},{}]"); reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$[0]"); reader.beginObject(); @@ -82,7 +118,7 @@ public final class JsonReaderPathTest { } @Test public void arrayOfArrays() throws IOException { - JsonReader reader = newReader("[[],[],[]]"); + JsonReader reader = factory.newReader("[[],[],[]]"); reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$[0]"); reader.beginArray(); @@ -102,7 +138,7 @@ public final class JsonReaderPathTest { } @Test public void objectPath() throws IOException { - JsonReader reader = newReader("{\"a\":1,\"b\":2}"); + JsonReader reader = factory.newReader("{\"a\":1,\"b\":2}"); assertThat(reader.getPath()).isEqualTo("$"); reader.peek(); @@ -142,7 +178,7 @@ public final class JsonReaderPathTest { } @Test public void arrayPath() throws IOException { - JsonReader reader = newReader("[1,2]"); + JsonReader reader = factory.newReader("[1,2]"); assertThat(reader.getPath()).isEqualTo("$"); reader.peek(); @@ -172,7 +208,9 @@ public final class JsonReaderPathTest { } @Test public void multipleTopLevelValuesInOneDocument() throws IOException { - JsonReader reader = newReader("[][]"); + assumeTrue(factory != Factory.JSON_OBJECT); + + JsonReader reader = factory.newReader("[][]"); reader.setLenient(true); reader.beginArray(); reader.endArray(); @@ -183,7 +221,7 @@ public final class JsonReaderPathTest { } @Test public void skipArrayElements() throws IOException { - JsonReader reader = newReader("[1,2,3]"); + JsonReader reader = factory.newReader("[1,2,3]"); reader.beginArray(); reader.skipValue(); reader.skipValue(); @@ -191,14 +229,14 @@ public final class JsonReaderPathTest { } @Test public void skipObjectNames() throws IOException { - JsonReader reader = newReader("{\"a\":1}"); + JsonReader reader = factory.newReader("{\"a\":1}"); reader.beginObject(); reader.skipValue(); assertThat(reader.getPath()).isEqualTo("$.null"); } @Test public void skipObjectValues() throws IOException { - JsonReader reader = newReader("{\"a\":1,\"b\":2}"); + JsonReader reader = factory.newReader("{\"a\":1,\"b\":2}"); reader.beginObject(); reader.nextName(); reader.skipValue(); @@ -208,7 +246,7 @@ public final class JsonReaderPathTest { } @Test public void skipNestedStructures() throws IOException { - JsonReader reader = newReader("[[1,2,3],4]"); + JsonReader reader = factory.newReader("[[1,2,3],4]"); reader.beginArray(); reader.skipValue(); assertThat(reader.getPath()).isEqualTo("$[1]"); diff --git a/moshi/src/test/java/com/squareup/moshi/ObjectJsonReaderTest.java b/moshi/src/test/java/com/squareup/moshi/ObjectJsonReaderTest.java new file mode 100644 index 0000000..6bf30be --- /dev/null +++ b/moshi/src/test/java/com/squareup/moshi/ObjectJsonReaderTest.java @@ -0,0 +1,325 @@ +/* + * 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.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +public final class ObjectJsonReaderTest { + @Test public void array() throws Exception { + List root = new ArrayList<>(); + root.add("s"); + root.add(1.5d); + root.add(true); + root.add(null); + JsonReader reader = new ObjectJsonReader(root); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_ARRAY); + reader.beginArray(); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); + assertThat(reader.nextString()).isEqualTo("s"); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.NUMBER); + assertThat(reader.nextDouble()).isEqualTo(1.5d); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.BOOLEAN); + assertThat(reader.nextBoolean()).isEqualTo(true); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.NULL); + assertThat(reader.nextNull()).isNull(); + + assertThat(reader.hasNext()).isFalse(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_ARRAY); + reader.endArray(); + + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void object() throws Exception { + Map root = new LinkedHashMap<>(); + root.put("a", "s"); + root.put("b", 1.5d); + root.put("c", true); + root.put("d", null); + JsonReader reader = new ObjectJsonReader(root); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT); + reader.beginObject(); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); + assertThat(reader.nextName()).isEqualTo("a"); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); + assertThat(reader.nextString()).isEqualTo("s"); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); + assertThat(reader.nextName()).isEqualTo("b"); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.NUMBER); + assertThat(reader.nextDouble()).isEqualTo(1.5d); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); + assertThat(reader.nextName()).isEqualTo("c"); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.BOOLEAN); + assertThat(reader.nextBoolean()).isEqualTo(true); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); + assertThat(reader.nextName()).isEqualTo("d"); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.NULL); + assertThat(reader.nextNull()).isNull(); + + assertThat(reader.hasNext()).isFalse(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_OBJECT); + reader.endObject(); + + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void nesting() throws Exception { + List>>> root + = Collections.singletonList(Collections.singletonMap( + "a", Collections.singletonList(Collections.singletonMap("b", 1.5d)))); + JsonReader reader = new ObjectJsonReader(root); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_ARRAY); + reader.beginArray(); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT); + reader.beginObject(); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); + assertThat(reader.nextName()).isEqualTo("a"); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_ARRAY); + reader.beginArray(); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT); + reader.beginObject(); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); + assertThat(reader.nextName()).isEqualTo("b"); + + assertThat(reader.hasNext()).isTrue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.NUMBER); + assertThat(reader.nextDouble()).isEqualTo(1.5d); + + assertThat(reader.hasNext()).isFalse(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_OBJECT); + reader.endObject(); + + assertThat(reader.hasNext()).isFalse(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_ARRAY); + reader.endArray(); + + assertThat(reader.hasNext()).isFalse(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_OBJECT); + reader.endObject(); + + assertThat(reader.hasNext()).isFalse(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_ARRAY); + reader.endArray(); + + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void promoteNameToValue() throws Exception { + Map root = Collections.singletonMap("a", "b"); + + JsonReader reader = new ObjectJsonReader(root); + reader.beginObject(); + reader.promoteNameToValue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); + assertThat(reader.nextString()).isEqualTo("a"); + + assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); + assertThat(reader.nextString()).isEqualTo("b"); + reader.endObject(); + + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void endArrayTooEarly() throws Exception { + JsonReader reader = new ObjectJsonReader(Collections.singletonList("s")); + + reader.beginArray(); + try { + reader.endArray(); + fail(); + } catch (JsonDataException expected) { + assertThat(expected).hasMessage("Expected END_ARRAY but was STRING at path $[0]"); + } + } + + @Test public void endObjectTooEarly() throws Exception { + JsonReader reader = new ObjectJsonReader(Collections.singletonMap("a", "b")); + + reader.beginObject(); + try { + reader.endObject(); + fail(); + } catch (JsonDataException expected) { + assertThat(expected).hasMessage("Expected END_OBJECT but was NAME at path $."); + } + } + + @Test public void unsupportedType() throws Exception { + JsonReader reader = new ObjectJsonReader(Collections.singletonList(new StringBuilder("x"))); + + reader.beginArray(); + try { + reader.peek(); + fail(); + } catch (JsonDataException expected) { + assertThat(expected).hasMessage( + "Expected a JSON value but was a java.lang.StringBuilder at path $[0]"); + } + } + + @Test public void skipRoot() throws Exception { + JsonReader reader = new ObjectJsonReader(Collections.singletonList(new StringBuilder("x"))); + reader.skipValue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void skipListValue() throws Exception { + List root = new ArrayList<>(); + root.add("a"); + root.add("b"); + root.add("c"); + JsonReader reader = new ObjectJsonReader(root); + + reader.beginArray(); + + assertThat(reader.getPath()).isEqualTo("$[0]"); + assertThat(reader.nextString()).isEqualTo("a"); + + assertThat(reader.getPath()).isEqualTo("$[1]"); + reader.skipValue(); + + assertThat(reader.getPath()).isEqualTo("$[2]"); + assertThat(reader.nextString()).isEqualTo("c"); + + reader.endArray(); + } + + @Test public void skipObjectName() throws Exception { + Map root = new LinkedHashMap<>(); + root.put("a", "s"); + root.put("b", 1.5d); + root.put("c", true); + JsonReader reader = new ObjectJsonReader(root); + + reader.beginObject(); + + assertThat(reader.nextName()).isEqualTo("a"); + assertThat(reader.getPath()).isEqualTo("$.a"); + assertThat(reader.nextString()).isEqualTo("s"); + assertThat(reader.getPath()).isEqualTo("$.a"); + + reader.skipValue(); + assertThat(reader.getPath()).isEqualTo("$.null"); + assertThat(reader.nextDouble()).isEqualTo(1.5d); + assertThat(reader.getPath()).isEqualTo("$.null"); + + assertThat(reader.nextName()).isEqualTo("c"); + assertThat(reader.getPath()).isEqualTo("$.c"); + assertThat(reader.nextBoolean()).isEqualTo(true); + assertThat(reader.getPath()).isEqualTo("$.c"); + + reader.endObject(); + } + + @Test public void skipObjectValue() throws Exception { + Map root = new LinkedHashMap<>(); + root.put("a", "s"); + root.put("b", 1.5d); + root.put("c", true); + JsonReader reader = new ObjectJsonReader(root); + + reader.beginObject(); + + assertThat(reader.nextName()).isEqualTo("a"); + assertThat(reader.getPath()).isEqualTo("$.a"); + assertThat(reader.nextString()).isEqualTo("s"); + assertThat(reader.getPath()).isEqualTo("$.a"); + + assertThat(reader.nextName()).isEqualTo("b"); + assertThat(reader.getPath()).isEqualTo("$.b"); + reader.skipValue(); + assertThat(reader.getPath()).isEqualTo("$.null"); + + assertThat(reader.nextName()).isEqualTo("c"); + assertThat(reader.getPath()).isEqualTo("$.c"); + assertThat(reader.nextBoolean()).isEqualTo(true); + assertThat(reader.getPath()).isEqualTo("$.c"); + + reader.endObject(); + } + + @Test public void failOnUnknown() throws Exception { + JsonReader reader = new ObjectJsonReader(Collections.singletonList("a")); + reader.setFailOnUnknown(true); + + reader.beginArray(); + try { + reader.skipValue(); + fail(); + } catch (JsonDataException expected) { + assertThat(expected).hasMessage("Cannot skip unexpected STRING at $[0]"); + } + } + + @Test public void close() throws Exception { + try { + JsonReader reader = new ObjectJsonReader(Collections.singletonList("a")); + reader.beginArray(); + reader.close(); + reader.nextString(); + fail(); + } catch (IllegalStateException expected) { + } + + try { + JsonReader reader = new ObjectJsonReader(Collections.singletonList("a")); + reader.close(); + reader.beginArray(); + fail(); + } catch (IllegalStateException expected) { + } + } +}