From b338d1e7edce1a26432f20a1202c6b2806b499d7 Mon Sep 17 00:00:00 2001 From: jwilson Date: Sun, 29 Jan 2017 22:18:12 -0500 Subject: [PATCH] New JsonAdapter.serializeNulls() method. This makes it possible to force nulls into the document without much fuss. --- .../moshi/BufferedSinkJsonWriter.java | 2 +- .../java/com/squareup/moshi/JsonAdapter.java | 25 +++ .../com/squareup/moshi/JsonAdapterTest.java | 167 ++++++++++++++++++ ...iterFactory.java => JsonCodecFactory.java} | 38 +++- .../com/squareup/moshi/JsonReaderFactory.java | 71 -------- .../squareup/moshi/JsonReaderPathTest.java | 6 +- .../com/squareup/moshi/JsonReaderTest.java | 5 +- .../squareup/moshi/JsonWriterPathTest.java | 6 +- .../com/squareup/moshi/JsonWriterTest.java | 6 +- 9 files changed, 235 insertions(+), 91 deletions(-) create mode 100644 moshi/src/test/java/com/squareup/moshi/JsonAdapterTest.java rename moshi/src/test/java/com/squareup/moshi/{JsonWriterFactory.java => JsonCodecFactory.java} (69%) delete mode 100644 moshi/src/test/java/com/squareup/moshi/JsonReaderFactory.java diff --git a/moshi/src/main/java/com/squareup/moshi/BufferedSinkJsonWriter.java b/moshi/src/main/java/com/squareup/moshi/BufferedSinkJsonWriter.java index 1260b81..cd5a834 100644 --- a/moshi/src/main/java/com/squareup/moshi/BufferedSinkJsonWriter.java +++ b/moshi/src/main/java/com/squareup/moshi/BufferedSinkJsonWriter.java @@ -200,7 +200,7 @@ final class BufferedSinkJsonWriter extends JsonWriter { } @Override public JsonWriter value(double value) throws IOException { - if (Double.isNaN(value) || Double.isInfinite(value)) { + if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) { throw new IllegalArgumentException("Numeric values must be finite, but was " + value); } if (promoteNameToValue) { diff --git a/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java index 0f40303..50d4813 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java @@ -83,6 +83,31 @@ public abstract class JsonAdapter { } } + /** + * Returns a JSON adapter equal to this JSON adapter, but that serializes nulls when encoding + * JSON. + */ + public final JsonAdapter serializeNulls() { + final JsonAdapter delegate = this; + return new JsonAdapter() { + @Override public T fromJson(JsonReader reader) throws IOException { + return delegate.fromJson(reader); + } + @Override public void toJson(JsonWriter writer, T value) throws IOException { + boolean serializeNulls = writer.getSerializeNulls(); + writer.setSerializeNulls(true); + try { + delegate.toJson(writer, value); + } finally { + writer.setSerializeNulls(serializeNulls); + } + } + @Override public String toString() { + return delegate + ".serializeNulls()"; + } + }; + } + /** * Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing * nulls. diff --git a/moshi/src/test/java/com/squareup/moshi/JsonAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/JsonAdapterTest.java new file mode 100644 index 0000000..fa32a6d --- /dev/null +++ b/moshi/src/test/java/com/squareup/moshi/JsonAdapterTest.java @@ -0,0 +1,167 @@ +/* + * 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.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +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 JsonAdapterTest { + @Parameter public JsonCodecFactory factory; + + @Parameters(name = "{0}") + public static List parameters() { + return JsonCodecFactory.factories(); + } + + @Test public void lenient() throws Exception { + JsonAdapter lenient = new JsonAdapter() { + @Override public Double fromJson(JsonReader reader) throws IOException { + return reader.nextDouble(); + } + + @Override public void toJson(JsonWriter writer, Double value) throws IOException { + writer.value(value); + } + }.lenient(); + + JsonReader reader = factory.newReader("[-Infinity, NaN, Infinity]"); + reader.beginArray(); + assertThat(lenient.fromJson(reader)).isEqualTo(Double.NEGATIVE_INFINITY); + assertThat(lenient.fromJson(reader)).isNaN(); + assertThat(lenient.fromJson(reader)).isEqualTo(Double.POSITIVE_INFINITY); + reader.endArray(); + + JsonWriter writer = factory.newWriter(); + writer.beginArray(); + lenient.toJson(writer, Double.NEGATIVE_INFINITY); + lenient.toJson(writer, Double.NaN); + lenient.toJson(writer, Double.POSITIVE_INFINITY); + writer.endArray(); + assertThat(factory.json()).isEqualTo("[-Infinity,NaN,Infinity]"); + } + + @Test public void nullSafe() throws Exception { + JsonAdapter toUpperCase = new JsonAdapter() { + @Override public String fromJson(JsonReader reader) throws IOException { + return reader.nextString().toUpperCase(Locale.US); + } + + @Override public void toJson(JsonWriter writer, String value) throws IOException { + writer.value(value.toUpperCase(Locale.US)); + } + }.nullSafe(); + + JsonReader reader = factory.newReader("[\"a\", null, \"c\"]"); + reader.beginArray(); + assertThat(toUpperCase.fromJson(reader)).isEqualTo("A"); + assertThat(toUpperCase.fromJson(reader)).isNull(); + assertThat(toUpperCase.fromJson(reader)).isEqualTo("C"); + reader.endArray(); + + JsonWriter writer = factory.newWriter(); + writer.beginArray(); + toUpperCase.toJson(writer, "a"); + toUpperCase.toJson(writer, null); + toUpperCase.toJson(writer, "c"); + writer.endArray(); + assertThat(factory.json()).isEqualTo("[\"A\",null,\"C\"]"); + } + + @Test public void failOnUnknown() throws Exception { + JsonAdapter alwaysSkip = new JsonAdapter() { + @Override public String fromJson(JsonReader reader) throws IOException { + reader.skipValue(); + throw new AssertionError(); + } + + @Override public void toJson(JsonWriter writer, String value) throws IOException { + throw new AssertionError(); + } + }.failOnUnknown(); + + JsonReader reader = factory.newReader("[\"a\"]"); + reader.beginArray(); + try { + alwaysSkip.fromJson(reader); + fail(); + } catch (JsonDataException expected) { + assertThat(expected).hasMessage("Cannot skip unexpected STRING at $[0]"); + } + assertThat(reader.nextString()).isEqualTo("a"); + reader.endArray(); + } + + @Test public void indent() throws Exception { + assumeTrue(factory.encodesToBytes()); + + JsonAdapter> indent = new JsonAdapter>() { + @Override public List fromJson(JsonReader reader) throws IOException { + throw new AssertionError(); + } + + @Override public void toJson(JsonWriter writer, List value) throws IOException { + writer.beginArray(); + for (String s : value) { + writer.value(s); + } + writer.endArray(); + } + }.indent("\t\t\t"); + + JsonWriter writer = factory.newWriter(); + indent.toJson(writer, Arrays.asList("a", "b", "c")); + assertThat(factory.json()).isEqualTo("" + + "[\n" + + "\t\t\t\"a\",\n" + + "\t\t\t\"b\",\n" + + "\t\t\t\"c\"\n" + + "]"); + } + + @Test public void serializeNulls() throws Exception { + JsonAdapter> serializeNulls = new JsonAdapter>() { + @Override public Map fromJson(JsonReader reader) throws IOException { + throw new AssertionError(); + } + + @Override public void toJson(JsonWriter writer, Map map) throws IOException { + writer.beginObject(); + for (Map.Entry entry : map.entrySet()) { + writer.name(entry.getKey()).value(entry.getValue()); + } + writer.endObject(); + } + }.serializeNulls(); + + JsonWriter writer = factory.newWriter(); + serializeNulls.toJson(writer, Collections.singletonMap("a", null)); + assertThat(factory.json()).isEqualTo("{\"a\":null}"); + } +} diff --git a/moshi/src/test/java/com/squareup/moshi/JsonWriterFactory.java b/moshi/src/test/java/com/squareup/moshi/JsonCodecFactory.java similarity index 69% rename from moshi/src/test/java/com/squareup/moshi/JsonWriterFactory.java rename to moshi/src/test/java/com/squareup/moshi/JsonCodecFactory.java index 820f8b7..758e6b8 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonWriterFactory.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonCodecFactory.java @@ -20,14 +20,19 @@ import java.util.Arrays; import java.util.List; import okio.Buffer; -abstract class JsonWriterFactory { +abstract class JsonCodecFactory { private static final Moshi MOSHI = new Moshi.Builder().build(); private static final JsonAdapter OBJECT_ADAPTER = MOSHI.adapter(Object.class); static List factories() { - final JsonWriterFactory bufferedSink = new JsonWriterFactory() { + final JsonCodecFactory bufferedSink = new JsonCodecFactory() { Buffer buffer; + @Override public JsonReader newReader(String json) { + Buffer buffer = new Buffer().writeUtf8(json); + return JsonReader.of(buffer); + } + @Override JsonWriter newWriter() { buffer = new Buffer(); return new BufferedSinkJsonWriter(buffer); @@ -39,18 +44,29 @@ abstract class JsonWriterFactory { return result; } - @Override boolean supportsMultipleTopLevelValuesInOneDocument() { + @Override boolean encodesToBytes() { return true; } @Override public String toString() { - return "BufferedSinkJsonWriter"; + return "Buffer"; } }; - final JsonWriterFactory object = new JsonWriterFactory() { + final JsonCodecFactory object = new JsonCodecFactory() { ObjectJsonWriter writer; + @Override public JsonReader newReader(String json) throws IOException { + Moshi moshi = new Moshi.Builder().build(); + Object object = moshi.adapter(Object.class).lenient().fromJson(json); + return new ObjectJsonReader(object); + } + + // TODO(jwilson): fix precision checks and delete his method. + @Override boolean implementsStrictPrecision() { + return false; + } + @Override JsonWriter newWriter() { writer = new ObjectJsonWriter(); return writer; @@ -62,6 +78,7 @@ abstract class JsonWriterFactory { Buffer buffer = new Buffer(); JsonWriter bufferedSinkWriter = JsonWriter.of(buffer); bufferedSinkWriter.setSerializeNulls(true); + bufferedSinkWriter.setLenient(true); OBJECT_ADAPTER.toJson(bufferedSinkWriter, writer.root()); return buffer.readUtf8(); } catch (IOException e) { @@ -75,7 +92,7 @@ abstract class JsonWriterFactory { } @Override public String toString() { - return "ObjectJsonWriter"; + return "Object"; } }; @@ -84,10 +101,17 @@ abstract class JsonWriterFactory { new Object[] { object }); } + abstract JsonReader newReader(String json) throws IOException; + abstract JsonWriter newWriter(); + + boolean implementsStrictPrecision() { + return true; + } + abstract String json(); - boolean supportsMultipleTopLevelValuesInOneDocument() { + boolean encodesToBytes() { return false; } diff --git a/moshi/src/test/java/com/squareup/moshi/JsonReaderFactory.java b/moshi/src/test/java/com/squareup/moshi/JsonReaderFactory.java deleted file mode 100644 index d8ca856..0000000 --- a/moshi/src/test/java/com/squareup/moshi/JsonReaderFactory.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.List; -import okio.Buffer; - -abstract class JsonReaderFactory { - static List factories() { - JsonReaderFactory bufferedSource = new JsonReaderFactory() { - @Override public JsonReader newReader(String json) { - Buffer buffer = new Buffer().writeUtf8(json); - return JsonReader.of(buffer); - } - - @Override boolean supportsMultipleTopLevelValuesInOneDocument() { - return true; - } - - @Override public String toString() { - return "BufferedSourceJsonReader"; - } - }; - - JsonReaderFactory jsonObject = new JsonReaderFactory() { - @Override public JsonReader newReader(String json) throws IOException { - Moshi moshi = new Moshi.Builder().build(); - Object object = moshi.adapter(Object.class).lenient().fromJson(json); - return new ObjectJsonReader(object); - } - - // TODO(jwilson): fix precision checks and delete his method. - @Override boolean implementsStrictPrecision() { - return false; - } - - @Override public String toString() { - return "ObjectJsonReader"; - } - }; - - return Arrays.asList( - new Object[] { bufferedSource }, - new Object[] { jsonObject }); - } - - abstract JsonReader newReader(String json) throws IOException; - - boolean supportsMultipleTopLevelValuesInOneDocument() { - return false; - } - - boolean implementsStrictPrecision() { - return true; - } -} diff --git a/moshi/src/test/java/com/squareup/moshi/JsonReaderPathTest.java b/moshi/src/test/java/com/squareup/moshi/JsonReaderPathTest.java index 7be0860..875232e 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonReaderPathTest.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonReaderPathTest.java @@ -28,11 +28,11 @@ import static org.junit.Assume.assumeTrue; @RunWith(Parameterized.class) public final class JsonReaderPathTest { - @Parameter public JsonReaderFactory factory; + @Parameter public JsonCodecFactory factory; @Parameters(name = "{0}") public static List parameters() { - return JsonReaderFactory.factories(); + return JsonCodecFactory.factories(); } @Test public void path() throws IOException { @@ -185,7 +185,7 @@ public final class JsonReaderPathTest { } @Test public void multipleTopLevelValuesInOneDocument() throws IOException { - assumeTrue(factory.supportsMultipleTopLevelValuesInOneDocument()); + assumeTrue(factory.encodesToBytes()); JsonReader reader = factory.newReader("[][]"); reader.setLenient(true); diff --git a/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java b/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java index 361635c..bfb5664 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java @@ -28,7 +28,6 @@ import static com.squareup.moshi.JsonReader.Token.BEGIN_ARRAY; import static com.squareup.moshi.JsonReader.Token.BEGIN_OBJECT; import static com.squareup.moshi.JsonReader.Token.NAME; import static com.squareup.moshi.JsonReader.Token.STRING; -import static com.squareup.moshi.TestUtil.newReader; import static com.squareup.moshi.TestUtil.repeat; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @@ -37,11 +36,11 @@ import static org.junit.Assume.assumeTrue; @RunWith(Parameterized.class) public final class JsonReaderTest { - @Parameter public JsonReaderFactory factory; + @Parameter public JsonCodecFactory factory; @Parameters(name = "{0}") public static List parameters() { - return JsonReaderFactory.factories(); + return JsonCodecFactory.factories(); } JsonReader newReader(String json) throws IOException { diff --git a/moshi/src/test/java/com/squareup/moshi/JsonWriterPathTest.java b/moshi/src/test/java/com/squareup/moshi/JsonWriterPathTest.java index bce20c5..ad9ca0c 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonWriterPathTest.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonWriterPathTest.java @@ -29,11 +29,11 @@ import static org.junit.Assume.assumeTrue; @RunWith(Parameterized.class) public final class JsonWriterPathTest { - @Parameter public JsonWriterFactory factory; + @Parameter public JsonCodecFactory factory; @Parameters(name = "{0}") public static List parameters() { - return JsonWriterFactory.factories(); + return JsonCodecFactory.factories(); } @Test public void path() throws IOException { @@ -202,7 +202,7 @@ public final class JsonWriterPathTest { } @Test public void multipleTopLevelValuesInOneDocument() throws IOException { - assumeTrue(factory.supportsMultipleTopLevelValuesInOneDocument()); + assumeTrue(factory.encodesToBytes()); JsonWriter writer = factory.newWriter(); writer.setLenient(true); diff --git a/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java b/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java index 55abfa6..571f644 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java @@ -31,11 +31,11 @@ import static org.junit.Assume.assumeTrue; @RunWith(Parameterized.class) public final class JsonWriterTest { - @Parameter public JsonWriterFactory factory; + @Parameter public JsonCodecFactory factory; @Parameters(name = "{0}") public static List parameters() { - return JsonWriterFactory.factories(); + return JsonCodecFactory.factories(); } @Test public void nullsValuesNotSerializedByDefault() throws IOException { @@ -469,7 +469,7 @@ public final class JsonWriterTest { } @Test public void lenientWriterPermitsMultipleTopLevelValues() throws IOException { - assumeTrue(factory.supportsMultipleTopLevelValuesInOneDocument()); + assumeTrue(factory.encodesToBytes()); JsonWriter writer = factory.newWriter(); writer.setLenient(true);