diff --git a/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java index 8e2755f..7520c3f 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import okio.Buffer; import okio.BufferedSink; import okio.BufferedSource; @@ -30,13 +31,21 @@ public abstract class JsonAdapter { public abstract T fromJson(JsonReader reader) throws IOException; public final T fromJson(BufferedSource source) throws IOException { - return fromJson(JsonReader.of(source)); + return fromJson(new BufferedSourceJsonReader(source)); } public final T fromJson(String string) throws IOException { return fromJson(new Buffer().writeUtf8(string)); } + public final T fromJsonTree(Object o) { + try { + return fromJson(new ObjectJsonReader(o)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + public abstract void toJson(JsonWriter writer, T value) throws IOException; public final void toJson(BufferedSink sink, T value) throws IOException { @@ -54,6 +63,16 @@ public abstract class JsonAdapter { return buffer.readUtf8(); } + public final Object toJsonTree(T value) { + AtomicReference sink = new AtomicReference<>(); + try { + toJson(new ObjectJsonWriter(sink), value); + } catch (IOException e) { + throw new IllegalStateException(e); + } + return sink.get(); + } + /** * Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing * nulls. 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..d285a28 --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/ObjectJsonReader.java @@ -0,0 +1,551 @@ +/* + * Copyright (C) 2016 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.Iterator; +import java.util.Map; + +final class ObjectJsonReader extends JsonReader { + private static final int PEEKED_NONE = 0; + private static final int PEEKED_MAP = 1; + private static final int PEEKED_MAP_ENTRY = 2; + private static final int PEEKED_MAP_ITERATOR_EXHAUSTED = 3; + private static final int PEEKED_LIST = 4; + private static final int PEEKED_LIST_ITERATOR_EXHAUSTED = 5; + private static final int PEEKED_BOOLEAN = 6; + private static final int PEEKED_NULL = 7; + private static final int PEEKED_STRING = 8; + private static final int PEEKED_INT = 9; + private static final int PEEKED_LONG = 10; + private static final int PEEKED_FLOAT = 11; + private static final int PEEKED_DOUBLE = 12; + private static final int PEEKED_DONE = 13; + + /** True to accept non-spec compliant JSON */ + private boolean lenient = false; + + /** True to throw a {@link JsonDataException} on any attempt to call {@link #skipValue()}. */ + private boolean failOnUnknown = false; + + private int peeked = PEEKED_NONE; + + /* + * The nesting stack. Using a manual array rather than an ArrayList saves 20%. + */ + private int[] stack = new int[32]; + private int stackSize = 0; + { + stack[stackSize++] = JsonScope.EMPTY_DOCUMENT; + } + + private String[] pathNames = new String[32]; + private int[] pathIndices = new int[32]; + + private final Object source; + private Object[] objects = new Object[32]; + private int objectsSize = 1; + + ObjectJsonReader(Object source) { + this.source = source; + this.objects[0] = source; + } + + @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; + } + + private void pushObject(Object newTop) { + if (objectsSize == objects.length) { + Object[] newObjects = new Object[objectsSize * 2]; + System.arraycopy(objects, 0, newObjects, 0, objectsSize); + objects = newObjects; + } + objects[objectsSize++] = newTop; + } + + private Object popObject() { + Object object = objects[objectsSize - 1]; + objects[objectsSize - 1] = null; // Free the object so that it can be garbage collected! + objectsSize--; + return object; + } + + @Override public void beginArray() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + if (p == PEEKED_LIST) { + Iterable iterable = (Iterable) popObject(); + pushObject(iterable.iterator()); + + push(JsonScope.EMPTY_ARRAY); + pathIndices[stackSize - 1] = 0; + peeked = PEEKED_NONE; + } else { + throw new JsonDataException("Expected BEGIN_ARRAY but was " + peek() + + " at path " + getPath()); + } + } + + @Override public void endArray() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + if (p == PEEKED_LIST_ITERATOR_EXHAUSTED) { + popObject(); + stackSize--; + pathIndices[stackSize - 1]++; + peeked = PEEKED_NONE; + } else { + throw new JsonDataException("Expected END_ARRAY but was " + peek() + + " at path " + getPath()); + } + } + + @Override public void beginObject() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + if (p == PEEKED_MAP) { + Map map = (Map) popObject(); + pushObject(map.entrySet().iterator()); + + push(JsonScope.EMPTY_OBJECT); + peeked = PEEKED_NONE; + } else { + throw new JsonDataException("Expected BEGIN_OBJECT but was " + peek() + + " at path " + getPath()); + } + } + + @Override public void endObject() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + if (p == PEEKED_MAP_ITERATOR_EXHAUSTED) { + popObject(); + stackSize--; + pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected! + pathIndices[stackSize - 1]++; + peeked = PEEKED_NONE; + } else { + throw new JsonDataException("Expected END_OBJECT but was " + peek() + + " at path " + getPath()); + } + } + + @Override public boolean hasNext() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + return p != PEEKED_LIST_ITERATOR_EXHAUSTED && p != PEEKED_MAP_ITERATOR_EXHAUSTED; + } + + @Override public Token peek() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + + switch (p) { + case PEEKED_MAP: + return Token.BEGIN_OBJECT; + case PEEKED_MAP_ITERATOR_EXHAUSTED: + return Token.END_OBJECT; + case PEEKED_LIST: + return Token.BEGIN_ARRAY; + case PEEKED_LIST_ITERATOR_EXHAUSTED: + return Token.END_ARRAY; + case PEEKED_MAP_ENTRY: + return Token.NAME; + case PEEKED_BOOLEAN: + return Token.BOOLEAN; + case PEEKED_NULL: + return Token.NULL; + case PEEKED_STRING: + return Token.STRING; + case PEEKED_INT: + case PEEKED_LONG: + case PEEKED_FLOAT: + case PEEKED_DOUBLE: + return Token.NUMBER; + case PEEKED_DONE: + return Token.END_DOCUMENT; + default: + throw new AssertionError(); + } + } + + private int doPeek() { + int peekStack = stack[stackSize - 1]; + if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) { + stack[stackSize - 1] = JsonScope.NONEMPTY_ARRAY; + + Iterator iterator = (Iterator) objects[objectsSize - 1]; + if (!iterator.hasNext()) { + return peeked = PEEKED_LIST_ITERATOR_EXHAUSTED; + } + + pushObject(iterator.next()); + } else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) { + stack[stackSize - 1] = JsonScope.DANGLING_NAME; + + Iterator> iterator = (Iterator>) objects[objectsSize - 1]; + if (!iterator.hasNext()) { + return peeked = PEEKED_MAP_ITERATOR_EXHAUSTED; + } + pushObject(iterator.next()); + return peeked = PEEKED_MAP_ENTRY; + } else if (peekStack == JsonScope.DANGLING_NAME) { + stack[stackSize - 1] = JsonScope.NONEMPTY_OBJECT; + } else if (peekStack == JsonScope.EMPTY_DOCUMENT) { + stack[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT; + } else if (peekStack == JsonScope.CLOSED) { + throw new IllegalStateException("JsonReader is closed"); + } + + if (objectsSize == 0) { + return peeked = PEEKED_DONE; + } + + Object object = objects[objectsSize - 1]; + if (object == null) { + return peeked = PEEKED_NULL; + } + if (object instanceof Boolean) { + return peeked = PEEKED_BOOLEAN; + } + if (object instanceof String) { + return peeked = PEEKED_STRING; + } + if (object instanceof Integer) { + return peeked = PEEKED_INT; + } + if (object instanceof Long) { + return peeked = PEEKED_LONG; + } + if (object instanceof Float) { + return peeked = PEEKED_FLOAT; + } + if (object instanceof Double) { + return peeked = PEEKED_DOUBLE; + } + if (object instanceof Iterable) { + return peeked = PEEKED_LIST; + } + if (object instanceof Map) { + return peeked = PEEKED_MAP; + } + throw syntaxError("Unrecognized type " + object.getClass().getName() + ": " + object); + } + + @Override public String nextName() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + String result; + if (p == PEEKED_MAP_ENTRY) { + Map.Entry object = (Map.Entry) popObject(); + result = (String) object.getKey(); + pushObject(object.getValue()); + } else { + throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath()); + } + peeked = PEEKED_NONE; + pathNames[stackSize - 1] = result; + return result; + } + + @Override public String nextString() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + String result; + if (p == PEEKED_STRING + || p == PEEKED_INT + || p == PEEKED_LONG + || p == PEEKED_FLOAT + || p == PEEKED_DOUBLE) { + result = popObject().toString(); + } else { + throw new JsonDataException("Expected a string but was " + peek() + " at path " + getPath()); + } + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } + + @Override public boolean nextBoolean() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + boolean result; + if (p == PEEKED_BOOLEAN) { + result = (boolean) popObject(); + } else { + throw new JsonDataException("Expected a boolean but was " + peek() + " at path " + getPath()); + } + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } + + @Override public T nextNull() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + if (p == PEEKED_NULL) { + popObject(); + } else { + throw new JsonDataException("Expected null but was " + peek() + " at path " + getPath()); + } + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return null; + } + + @Override public double nextDouble() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + Object object = popObject(); + + double result; + if (p == PEEKED_INT) { + result = (int) object; + } else if (p == PEEKED_LONG) { + result = (long) object; + } else if (p == PEEKED_FLOAT) { + result = (float) object; + } else if (p == PEEKED_DOUBLE) { + result = (double) object; + } else if (p == PEEKED_STRING) { + String string = object.toString(); + try { + result = Double.parseDouble(string); + } catch (NumberFormatException e) { + throw new JsonDataException( + "Expected a double but was " + string + " at path " + getPath()); + } + } else { + throw new JsonDataException("Expected a double but was " + peek() + " at path " + getPath()); + } + + if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) { + throw new IllegalStateException( + "JSON forbids NaN and infinities: " + result + " at path " + getPath()); + } + + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } + + @Override public long nextLong() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + + Object object = popObject(); + + long result; + if (p == PEEKED_INT) { + result = (int) object; + } else if (p == PEEKED_LONG) { + result = (long) object; + } else if (p == PEEKED_FLOAT) { + result = ((Float) object).longValue(); + } else if (p == PEEKED_DOUBLE) { + result = ((Double) object).longValue(); + } else if (p == PEEKED_STRING) { + String string = object.toString(); + try { + result = Long.parseLong(string); + } catch (NumberFormatException ignored) { + double asDouble; + try { + asDouble = Double.parseDouble(string); + } catch (NumberFormatException e) { + throw new JsonDataException( + "Expected a long but was " + string + " at path " + getPath()); + } + result = (long) asDouble; + if (result != asDouble) { // Make sure no precision was lost casting to 'long'. + throw new JsonDataException( + "Expected a long but was " + string + " at path " + getPath()); + } + } + } else { + throw new JsonDataException("Expected a long but was " + peek() + " at path " + getPath()); + } + + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } + + @Override public int nextInt() { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + + Object object = popObject(); + + int result; + if (p == PEEKED_INT) { + result = (int) object; + } else if (p == PEEKED_LONG) { + long asLong = (long) object; + result = (int) asLong; + if (result != asLong) { // Make sure no precision was lost casting to 'int'. + throw new JsonDataException("Expected an int but was " + object + " at path " + getPath()); + } + } else if (p == PEEKED_FLOAT) { + float asFloat = (float) object; + result = (int) asFloat; + if (result != asFloat) { // Make sure no precision was lost casting to 'int'. + throw new JsonDataException("Expected an int but was " + object + " at path " + getPath()); + } + } else if (p == PEEKED_DOUBLE) { + double asDouble = (double) object; + result = (int) asDouble; + if (result != asDouble) { // Make sure no precision was lost casting to 'int'. + throw new JsonDataException("Expected an int but was " + object + " at path " + getPath()); + } + } else if (p == PEEKED_STRING) { + String string = object.toString(); + try { + result = Integer.parseInt(string); + } catch (NumberFormatException ignored) { + double asDouble; + try { + asDouble = Double.parseDouble(string); + } catch (NumberFormatException e) { + throw new JsonDataException( + "Expected an int but was " + string + " at path " + getPath()); + } + result = (int) asDouble; + if (result != asDouble) { // Make sure no precision was lost casting to 'int'. + throw new JsonDataException( + "Expected an int but was " + string + " at path " + getPath()); + } + } + } else { + throw new JsonDataException("Expected an int but was " + peek() + " at path " + getPath()); + } + + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } + + @Override public void close() { + peeked = PEEKED_NONE; + stack[0] = JsonScope.CLOSED; + stackSize = 1; + objects = null; + objectsSize = -1; + } + + @Override public void skipValue() { + if (failOnUnknown) { + throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath()); + } + int count = 0; + do { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + + if (p == PEEKED_LIST_ITERATOR_EXHAUSTED) { + stackSize--; + count--; + } else if (p == PEEKED_MAP_ITERATOR_EXHAUSTED) { + stackSize--; + count--; + } else { + popObject(); + } + peeked = PEEKED_NONE; + } while (count != 0); + + pathIndices[stackSize - 1]++; + pathNames[stackSize - 1] = "null"; + } + + private void push(int newTop) { + if (stackSize == stack.length) { + int[] newStack = new int[stackSize * 2]; + int[] newPathIndices = new int[stackSize * 2]; + String[] newPathNames = new String[stackSize * 2]; + System.arraycopy(stack, 0, newStack, 0, stackSize); + System.arraycopy(pathIndices, 0, newPathIndices, 0, stackSize); + System.arraycopy(pathNames, 0, newPathNames, 0, stackSize); + stack = newStack; + pathIndices = newPathIndices; + pathNames = newPathNames; + } + stack[stackSize++] = newTop; + } + + @Override public String toString() { + return "JsonReader(" + source + ")"; + } + + @Override public String getPath() { + return JsonScope.getPath(stackSize, stack, pathNames, pathIndices); + } + + /** + * Throws a new IO exception with the given message and a context snippet + * with this reader's content. + */ + private IllegalStateException syntaxError(String message) { + return new IllegalStateException(message + " at path " + getPath()); + } + + @Override void promoteNameToValue() { + if (hasNext()) { + pushObject(nextName()); + peeked = PEEKED_STRING; + } + } +} diff --git a/moshi/src/main/java/com/squareup/moshi/ObjectJsonWriter.java b/moshi/src/main/java/com/squareup/moshi/ObjectJsonWriter.java new file mode 100644 index 0000000..1e63767 --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/ObjectJsonWriter.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2016 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.LinkedHashMap; +import java.util.concurrent.atomic.AtomicReference; + +import static com.squareup.moshi.JsonScope.DANGLING_NAME; +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_ARRAY; +import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT; +import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT; + +public final class ObjectJsonWriter extends JsonWriter { + private final AtomicReference sink; + + private int[] stack = new int[32]; + private int stackSize = 0; + { + push(EMPTY_DOCUMENT); + } + + private String[] pathNames = new String[32]; + private int[] pathIndices = new int[32]; + + private Object[] objects = new Object[32]; + private int objectsSize = 1; + + private boolean lenient; + + private String deferredName; + + private boolean serializeNulls; + + private boolean promoteNameToValue; + + public ObjectJsonWriter(AtomicReference sink) { + if (sink == null) throw new NullPointerException("sink == null"); + this.sink = sink; + } + + private void pushObject(Object newTop) { + if (objectsSize == objects.length) { + Object[] newObjects = new Object[objectsSize * 2]; + System.arraycopy(objects, 0, newObjects, 0, objectsSize); + objects = newObjects; + } + objects[objectsSize++] = newTop; + } + + private Object popObject() { + Object object = objects[objectsSize - 1]; + objects[objectsSize - 1] = null; // Free the object so that it can be garbage collected! + objectsSize--; + return object; + } + + @Override public void setIndent(String indent) { + // Ignored + } + + @Override public final void setLenient(boolean lenient) { + this.lenient = lenient; + } + + @Override public boolean isLenient() { + return lenient; + } + + @Override public final void setSerializeNulls(boolean serializeNulls) { + this.serializeNulls = serializeNulls; + } + + @Override public final boolean getSerializeNulls() { + return serializeNulls; + } + + @Override public JsonWriter beginArray() { + pushObject(new LinkedHashMap()); + return open(EMPTY_ARRAY); + } + + @Override public JsonWriter endArray() { + return close(EMPTY_ARRAY, NONEMPTY_ARRAY); + } + + @Override public JsonWriter beginObject() { + pushObject(new ArrayList<>()); + return open(EMPTY_OBJECT); + } + + @Override public JsonWriter endObject() { + return null; + } + + /** + * Enters a new scope by appending any necessary whitespace and the given + * bracket. + */ + private JsonWriter open(int empty) { + beforeValue(); + pathIndices[stackSize] = 0; + push(empty); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the + * given bracket. + */ + private JsonWriter close(int empty, int nonempty) { + int context = peek(); + if (context != nonempty && context != empty) { + throw new IllegalStateException("Nesting problem."); + } + if (deferredName != null) { + throw new IllegalStateException("Dangling name: " + deferredName); + } + + stackSize--; + pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected! + pathIndices[stackSize - 1]++; + return this; + } + + private void push(int newTop) { + if (stackSize == stack.length) { + int[] newStack = new int[stackSize * 2]; + System.arraycopy(stack, 0, newStack, 0, stackSize); + stack = newStack; + } + stack[stackSize++] = newTop; + } + + /** + * Returns the value on the top of the stack. + */ + private int peek() { + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + return stack[stackSize - 1]; + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(int topOfStack) { + stack[stackSize - 1] = topOfStack; + } + + @Override public JsonWriter name(String name) { + return null; + } + + private void writeDeferredName() throws IOException { + if (deferredName != null) { + // TODO + deferredName = null; + } + } + + @Override public JsonWriter value(String value) { + return null; + } + + @Override public JsonWriter nullValue() { + return null; + } + + @Override public JsonWriter value(boolean value) { + return null; + } + + @Override public JsonWriter value(double value) { + return null; + } + + @Override public JsonWriter value(long value) { + return null; + } + + @Override public JsonWriter value(Number value) { + return null; + } + + /** + * Inserts any necessary separators and whitespace before a literal value, + * inline array, or inline object. Also adjusts the stack to expect either a + * closing bracket or another element. + */ + @SuppressWarnings("fallthrough") + private void beforeValue() { + switch (peek()) { + case NONEMPTY_DOCUMENT: + if (!lenient) { + throw new IllegalStateException( + "JSON must have only one top-level value."); + } + // fall-through + case EMPTY_DOCUMENT: // first in document + replaceTop(NONEMPTY_DOCUMENT); + break; + + case EMPTY_ARRAY: // first in array + replaceTop(NONEMPTY_ARRAY); + break; + + case NONEMPTY_ARRAY: // another in array + break; + + case DANGLING_NAME: // value for name + replaceTop(NONEMPTY_OBJECT); + break; + + default: + throw new IllegalStateException("Nesting problem."); + } + } + + @Override public String getPath() { + return JsonScope.getPath(stackSize, stack, pathNames, pathIndices); + } + + @Override void promoteNameToValue() { + int context = peek(); + if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) { + throw new IllegalStateException("Nesting problem."); + } + promoteNameToValue = true; + } + + @Override public void close() throws IOException { + int size = stackSize; + if (size > 1 || size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT) { + throw new IOException("Incomplete document"); + } + stackSize = 0; + } + + @Override public void flush() { + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + } +} diff --git a/moshi/src/test/java/com/squareup/moshi/BufferedSourceJsonReaderTest.java b/moshi/src/test/java/com/squareup/moshi/BufferedSourceJsonReaderTest.java index 913d4c0..251b3cc 100644 --- a/moshi/src/test/java/com/squareup/moshi/BufferedSourceJsonReaderTest.java +++ b/moshi/src/test/java/com/squareup/moshi/BufferedSourceJsonReaderTest.java @@ -719,30 +719,30 @@ public final class BufferedSourceJsonReaderTest { } @Test public void prematurelyClosed() throws IOException { + JsonReader reader1 = newReader("{\"a\":[]}"); + reader1.beginObject(); + reader1.close(); try { - JsonReader reader = newReader("{\"a\":[]}"); - reader.beginObject(); - reader.close(); - reader.nextName(); + reader1.nextName(); fail(); } catch (IllegalStateException expected) { } + JsonReader reader2 = newReader("{\"a\":[]}"); + reader2.close(); try { - JsonReader reader = newReader("{\"a\":[]}"); - reader.close(); - reader.beginObject(); + reader2.beginObject(); fail(); } catch (IllegalStateException expected) { } + JsonReader reader3 = newReader("{\"a\":true}"); + reader3.beginObject(); + reader3.nextName(); + reader3.peek(); + reader3.close(); try { - JsonReader reader = newReader("{\"a\":true}"); - reader.beginObject(); - reader.nextName(); - reader.peek(); - reader.close(); - reader.nextBoolean(); + reader3.nextBoolean(); fail(); } catch (IllegalStateException expected) { } 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..010649c --- /dev/null +++ b/moshi/src/test/java/com/squareup/moshi/ObjectJsonReaderTest.java @@ -0,0 +1,389 @@ +package com.squareup.moshi; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.Test; + +import static com.squareup.moshi.JsonReader.Token.BEGIN_ARRAY; +import static com.squareup.moshi.JsonReader.Token.BEGIN_OBJECT; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +public final class ObjectJsonReaderTest { + @Test public void readArray() throws IOException { + JsonReader reader = new ObjectJsonReader(Arrays.asList(true, true)); + reader.beginArray(); + assertThat(reader.nextBoolean()).isTrue(); + assertThat(reader.nextBoolean()).isTrue(); + reader.endArray(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void readEmptyArray() throws IOException { + JsonReader reader = new ObjectJsonReader(emptyList()); + reader.beginArray(); + assertThat(reader.hasNext()).isFalse(); + reader.endArray(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void readObject() throws IOException { + Map object = new LinkedHashMap<>(); + object.put("a", "android"); + object.put("b", "banana"); + JsonReader reader = new ObjectJsonReader(object); + reader.beginObject(); + assertThat(reader.nextName()).isEqualTo("a"); + assertThat(reader.nextString()).isEqualTo("android"); + assertThat(reader.nextName()).isEqualTo("b"); + assertThat(reader.nextString()).isEqualTo("banana"); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void readEmptyObject() throws IOException { + JsonReader reader = new ObjectJsonReader(Collections.emptyMap()); + reader.beginObject(); + assertThat(reader.hasNext()).isFalse(); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void skipArray() throws IOException { + Map object = new LinkedHashMap<>(); + object.put("a", Arrays.asList("one", "two", "three")); + object.put("b", 123); + JsonReader reader = new ObjectJsonReader(object); + reader.beginObject(); + assertThat(reader.nextName()).isEqualTo("a"); + reader.skipValue(); + assertThat(reader.nextName()).isEqualTo("b"); + assertThat(reader.nextInt()).isEqualTo(123); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void skipArrayAfterPeek() throws IOException { + Map object = new LinkedHashMap<>(); + object.put("a", Arrays.asList("one", "two", "three")); + object.put("b", 123); + JsonReader reader = new ObjectJsonReader(object); + reader.beginObject(); + assertThat(reader.nextName()).isEqualTo("a"); + assertThat(reader.peek()).isEqualTo(BEGIN_ARRAY); + reader.skipValue(); + assertThat(reader.nextName()).isEqualTo("b"); + assertThat(reader.nextInt()).isEqualTo(123); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void skipTopLevelObject() throws IOException { + Map object = new LinkedHashMap<>(); + object.put("a", Arrays.asList("one", "two", "three")); + object.put("b", 123); + JsonReader reader = new ObjectJsonReader(object); + reader.skipValue(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void skipObject() throws IOException { + Map object = new LinkedHashMap<>(); + Map nestedObject = new LinkedHashMap<>(); + nestedObject.put("c", emptyList()); + nestedObject.put("d", Arrays.asList(true, true, Collections.emptyMap())); + object.put("a", nestedObject); + object.put("b", "banana"); + JsonReader reader = new ObjectJsonReader(object); + reader.beginObject(); + assertThat(reader.nextName()).isEqualTo("a"); + reader.skipValue(); + assertThat(reader.nextName()).isEqualTo("b"); + reader.skipValue(); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void skipObjectAfterPeek() throws IOException { + Map object = new LinkedHashMap<>(); + object.put("one", singletonMap("num", 1)); + object.put("two", singletonMap("num", 2)); + object.put("three", singletonMap("num", 3)); + JsonReader reader = new ObjectJsonReader(object); + reader.beginObject(); + assertThat(reader.nextName()).isEqualTo("one"); + assertThat(reader.peek()).isEqualTo(BEGIN_OBJECT); + reader.skipValue(); + assertThat(reader.nextName()).isEqualTo("two"); + assertThat(reader.peek()).isEqualTo(BEGIN_OBJECT); + reader.skipValue(); + assertThat(reader.nextName()).isEqualTo("three"); + reader.skipValue(); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void skipInteger() throws IOException { + Map object = new LinkedHashMap<>(); + object.put("a", 123456789); + object.put("b", -123456789); + JsonReader reader = new ObjectJsonReader(object); + reader.beginObject(); + assertThat(reader.nextName()).isEqualTo("a"); + reader.skipValue(); + assertThat(reader.nextName()).isEqualTo("b"); + reader.skipValue(); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void skipDouble() throws IOException { + Map object = new LinkedHashMap<>(); + object.put("a", Double.MIN_VALUE); + object.put("b", Double.MAX_VALUE); + JsonReader reader = new ObjectJsonReader(object); + reader.beginObject(); + assertThat(reader.nextName()).isEqualTo("a"); + reader.skipValue(); + assertThat(reader.nextName()).isEqualTo("b"); + reader.skipValue(); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void failOnUnknownFailsOnUnknownObjectValue() throws IOException { + Map object = Collections.singletonMap("a", 123); + JsonReader reader = new ObjectJsonReader(object); + reader.setFailOnUnknown(true); + reader.beginObject(); + assertThat(reader.nextName()).isEqualTo("a"); + try { + reader.skipValue(); + fail(); + } catch (JsonDataException expected) { + assertThat(expected).hasMessage("Cannot skip unexpected NUMBER at $.a"); + } + // Confirm that the reader is left in a consistent state after the exception. + reader.setFailOnUnknown(false); + assertThat(reader.nextInt()).isEqualTo(123); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void failOnUnknownFailsOnUnknownArrayElement() throws IOException { + JsonReader reader = new ObjectJsonReader(Arrays.asList("a", 123)); + reader.setFailOnUnknown(true); + reader.beginArray(); + assertThat(reader.nextString()).isEqualTo("a"); + try { + reader.skipValue(); + fail(); + } catch (JsonDataException expected) { + assertThat(expected).hasMessage("Cannot skip unexpected NUMBER at $[1]"); + } + // Confirm that the reader is left in a consistent state after the exception. + reader.setFailOnUnknown(false); + assertThat(reader.nextInt()).isEqualTo(123); + reader.endArray(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void helloWorld() throws IOException { + Map object = new LinkedHashMap<>(); + object.put("hello", true); + object.put("foo", Collections.singletonList("world")); + JsonReader reader = new ObjectJsonReader(object); + reader.beginObject(); + assertThat(reader.nextName()).isEqualTo("hello"); + assertThat(reader.nextBoolean()).isTrue(); + assertThat(reader.nextName()).isEqualTo("foo"); + reader.beginArray(); + assertThat(reader.nextString()).isEqualTo("world"); + reader.endArray(); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void integerFromOtherNumberTypes() throws IOException { + JsonReader reader = new ObjectJsonReader(Arrays.asList(1, 1L, 1.0f, 1.0)); + reader.beginArray(); + assertThat(reader.nextInt()).isEqualTo(1); + assertThat(reader.nextInt()).isEqualTo(1); + assertThat(reader.nextInt()).isEqualTo(1); + assertThat(reader.nextInt()).isEqualTo(1); + reader.endArray(); + } + + @Test public void longFromOtherNumberTypes() throws IOException { + JsonReader reader = new ObjectJsonReader(Arrays.asList(1, 1L, 1.0f, 1.0)); + reader.beginArray(); + assertThat(reader.nextLong()).isEqualTo(1L); + assertThat(reader.nextLong()).isEqualTo(1L); + assertThat(reader.nextLong()).isEqualTo(1L); + assertThat(reader.nextLong()).isEqualTo(1L); + reader.endArray(); + } + + @Test public void doubleFromOtherNumberTypes() throws IOException { + JsonReader reader = new ObjectJsonReader(Arrays.asList(1, 1L, 1.0f, 1.0)); + reader.beginArray(); + assertThat(reader.nextDouble()).isEqualTo(1.0); + assertThat(reader.nextDouble()).isEqualTo(1.0); + assertThat(reader.nextDouble()).isEqualTo(1.0); + assertThat(reader.nextDouble()).isEqualTo(1.0); + reader.endArray(); + } + + @Test public void stringFromNumberTypes() throws IOException { + JsonReader reader = new ObjectJsonReader(Arrays.asList(1, 1L, 1.0f, 1.0)); + reader.beginArray(); + assertThat(reader.nextString()).isEqualTo("1"); + assertThat(reader.nextString()).isEqualTo("1"); + assertThat(reader.nextString()).isEqualTo("1.0"); + assertThat(reader.nextString()).isEqualTo("1.0"); + reader.endArray(); + } + + @Test public void prematurelyClosed() throws IOException { + JsonReader reader1 = new ObjectJsonReader(singletonMap("a", emptyList())); + reader1.beginObject(); + reader1.close(); + try { + reader1.nextName(); + fail(); + } catch (IllegalStateException expected) { + } + + JsonReader reader2 = new ObjectJsonReader(singletonMap("a", emptyList())); + reader2.close(); + try { + reader2.beginObject(); + fail(); + } catch (IllegalStateException expected) { + } + + JsonReader reader3 = new ObjectJsonReader(singletonMap("a", true)); + reader3.beginObject(); + reader3.nextName(); + reader3.peek(); + reader3.close(); + try { + reader3.nextBoolean(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test public void nextFailuresDoNotAdvance() throws IOException { + JsonReader reader = new ObjectJsonReader(singletonMap("a", true)); + reader.beginObject(); + try { + reader.nextString(); + fail(); + } catch (JsonDataException expected) { + } + assertThat(reader.nextName()).isEqualTo("a"); + try { + reader.nextName(); + fail(); + } catch (JsonDataException expected) { + } + try { + reader.beginArray(); + fail(); + } catch (JsonDataException expected) { + } + try { + reader.endArray(); + fail(); + } catch (JsonDataException expected) { + } + try { + reader.beginObject(); + fail(); + } catch (JsonDataException expected) { + } + try { + reader.endObject(); + fail(); + } catch (JsonDataException expected) { + } + assertThat(reader.nextBoolean()).isTrue(); + try { + reader.nextString(); + fail(); + } catch (JsonDataException expected) { + } + try { + reader.nextName(); + fail(); + } catch (JsonDataException expected) { + } + try { + reader.beginArray(); + fail(); + } catch (JsonDataException expected) { + } + try { + reader.endArray(); + fail(); + } catch (JsonDataException expected) { + } + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + reader.close(); + } + + @Test public void integerMismatchWithDoubleDoesNotAdvance() { + + } + + @Test public void topLevelValueTypes() throws IOException { + JsonReader reader1 = new ObjectJsonReader(true); + assertThat(reader1.nextBoolean()).isTrue(); + assertThat(reader1.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + + JsonReader reader2 = new ObjectJsonReader(false); + assertThat(reader2.nextBoolean()).isFalse(); + assertThat(reader2.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + + JsonReader reader3 = new ObjectJsonReader(null); + assertThat(reader3.nextNull()).isNull(); + assertThat(reader3.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + + JsonReader reader4 = new ObjectJsonReader(123); + assertThat(reader4.nextLong()).isEqualTo(123); + assertThat(reader4.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + + JsonReader reader5 = new ObjectJsonReader(123.4); + assertThat(reader5.nextDouble()).isEqualTo(123.4); + assertThat(reader5.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + + JsonReader reader6 = new ObjectJsonReader("Hi"); + assertThat(reader6.nextString()).isEqualTo("Hi"); + assertThat(reader6.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void list() throws IOException { + JsonReader reader = new ObjectJsonReader(Arrays.asList("Hello", "World")); + reader.beginArray(); + assertThat(reader.nextString()).isEqualTo("Hello"); + assertThat(reader.nextString()).isEqualTo("World"); + reader.endArray(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void map() throws IOException { + JsonReader reader = new ObjectJsonReader(singletonMap("Hello", "World")); + reader.beginObject(); + assertThat(reader.nextName()).isEqualTo("Hello"); + assertThat(reader.nextString()).isEqualTo("World"); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } +} diff --git a/moshi/src/test/java/com/squareup/moshi/ObjectJsonWriterTest.java b/moshi/src/test/java/com/squareup/moshi/ObjectJsonWriterTest.java new file mode 100644 index 0000000..2e475f8 --- /dev/null +++ b/moshi/src/test/java/com/squareup/moshi/ObjectJsonWriterTest.java @@ -0,0 +1,9 @@ +package com.squareup.moshi; + +import org.junit.Test; + +public final class ObjectJsonWriterTest { + @Test public void topLevelValueTypes() { + + } +}