Merge pull request #20 from square/jwilson_0323_map_adapter

Map adapter.
This commit is contained in:
Jesse Wilson
2015-03-24 03:05:55 -04:00
7 changed files with 221 additions and 23 deletions

View File

@@ -46,7 +46,7 @@ class ArrayJsonAdapter extends JsonAdapter<Object> {
}
@Override public Object fromJson(JsonReader reader) throws IOException {
List<Object> list = new ArrayList<Object>();
List<Object> list = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
list.add(elementAdapter.fromJson(reader));

View File

@@ -46,7 +46,7 @@ abstract class ClassFactory<T> {
return (T) constructor.newInstance(args);
}
};
} catch (NoSuchMethodException noNoArgsConstructor) {
} catch (NoSuchMethodException ignored) {
// No no-args constructor. Fall back to something more magical...
}
@@ -68,7 +68,7 @@ abstract class ClassFactory<T> {
};
} catch (IllegalAccessException e) {
throw new AssertionError();
} catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException notJvm) {
} catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException ignored) {
// Not the expected version of the Oracle Java library!
}
@@ -95,7 +95,7 @@ abstract class ClassFactory<T> {
throw new AssertionError();
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException notLibcore) {
} catch (NoSuchMethodException ignored) {
// Not the expected version of Dalvik/libcore!
}

View File

@@ -29,7 +29,7 @@ import java.util.TreeMap;
* of classes in {@code java.*}, {@code javax.*} and {@code android.*} are omitted from both
* serialization and deserialization unless they are either public or protected.
*/
final class ClassAdapter<T> extends JsonAdapter<T> {
final class ClassJsonAdapter<T> extends JsonAdapter<T> {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, Moshi moshi) {
Class<?> rawType = Types.getRawType(type);
@@ -54,7 +54,7 @@ final class ClassAdapter<T> extends JsonAdapter<T> {
for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) {
createFieldBindings(moshi, t, fields);
}
return new ClassAdapter<>(classFactory, fields).nullSafe();
return new ClassJsonAdapter<>(classFactory, fields).nullSafe();
}
/** Creates a field binding for each of declared field of {@code type}. */
@@ -103,7 +103,7 @@ final class ClassAdapter<T> extends JsonAdapter<T> {
private final ClassFactory<T> classFactory;
private final Map<String, FieldBinding<?>> jsonFields;
private ClassAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> jsonFields) {
private ClassJsonAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> jsonFields) {
this.classFactory = classFactory;
this.jsonFields = jsonFields;
}

View File

@@ -49,7 +49,7 @@ abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAda
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
return new CollectionJsonAdapter<Collection<T>, T>(elementAdapter) {
@Override Collection<T> newCollection() {
return new ArrayList<T>();
return new ArrayList<>();
}
};
}
@@ -59,7 +59,7 @@ abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAda
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
return new CollectionJsonAdapter<Set<T>, T>(elementAdapter) {
@Override Set<T> newCollection() {
return new LinkedHashSet<T>();
return new LinkedHashSet<>();
}
};
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2015 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.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;
import java.util.Map;
/**
* Converts maps with string keys to JSON objects.
*
* TODO: support maps with other key types and convert to/from strings.
*/
final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
public static final Factory FACTORY = new Factory() {
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, Moshi moshi) {
Class<?> rawType = Types.getRawType(type);
if (rawType != Map.class) return null;
Type[] keyAndValue = Types.mapKeyAndValueTypes(type, rawType);
if (keyAndValue[0] != String.class) return null;
return new MapJsonAdapter<>(moshi, keyAndValue[1]).nullSafe();
};
};
private final JsonAdapter<V> valueAdapter;
public MapJsonAdapter(Moshi moshi, Type valueType) {
this.valueAdapter = moshi.adapter(valueType);
}
@Override public void toJson(JsonWriter writer, Map<K, V> map) throws IOException {
writer.beginObject();
for (Map.Entry<K, V> entry : map.entrySet()) {
writer.name((String) entry.getKey());
valueAdapter.toJson(writer, entry.getValue());
}
writer.endObject();
}
@Override public Map<K, V> fromJson(JsonReader reader) throws IOException {
LinkedHashTreeMap<K, V> result = new LinkedHashTreeMap<>();
reader.beginObject();
while (reader.hasNext()) {
@SuppressWarnings("unchecked") // Currently 'K' is always 'String'.
K name = (K) reader.nextName();
V value = valueAdapter.fromJson(reader);
V replaced = result.put(name, value);
if (replaced != null) {
throw new IllegalArgumentException("object property '" + name + "' has multiple values");
}
}
reader.endObject();
return result;
}
}

View File

@@ -28,7 +28,7 @@ import static com.squareup.moshi.Util.NO_ANNOTATIONS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
public final class ClassAdapterTest {
public final class ClassJsonAdapterTest {
private final Moshi moshi = new Moshi.Builder().build();
static class BasicPizza {
@@ -156,12 +156,12 @@ public final class ClassAdapterTest {
@Test public void fieldNameCollision() throws Exception {
try {
ClassAdapter.FACTORY.create(ExtendsBaseA.class, NO_ANNOTATIONS, moshi);
ClassJsonAdapter.FACTORY.create(ExtendsBaseA.class, NO_ANNOTATIONS, moshi);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("field name collision: 'a' declared by both "
+ "com.squareup.moshi.ClassAdapterTest$ExtendsBaseA and "
+ "superclass com.squareup.moshi.ClassAdapterTest$BaseA");
+ "com.squareup.moshi.ClassJsonAdapterTest$ExtendsBaseA and "
+ "superclass com.squareup.moshi.ClassJsonAdapterTest$BaseA");
}
}
@@ -302,17 +302,17 @@ public final class ClassAdapterTest {
@Test public void nonStaticNestedClassNotSupported() throws Exception {
try {
ClassAdapter.FACTORY.create(NonStatic.class, NO_ANNOTATIONS, moshi);
ClassJsonAdapter.FACTORY.create(NonStatic.class, NO_ANNOTATIONS, moshi);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("cannot serialize non-static nested class "
+ "com.squareup.moshi.ClassAdapterTest$NonStatic");
+ "com.squareup.moshi.ClassJsonAdapterTest$NonStatic");
}
}
@Test public void platformClassNotSupported() throws Exception {
assertThat(ClassAdapter.FACTORY.create(UUID.class, NO_ANNOTATIONS, moshi)).isNull();
assertThat(ClassAdapter.FACTORY.create(KeyGenerator.class, NO_ANNOTATIONS, moshi)).isNull();
assertThat(ClassJsonAdapter.FACTORY.create(UUID.class, NO_ANNOTATIONS, moshi)).isNull();
assertThat(ClassJsonAdapter.FACTORY.create(KeyGenerator.class, NO_ANNOTATIONS, moshi)).isNull();
}
@Test public void anonymousClassNotSupported() throws Exception {
@@ -322,7 +322,7 @@ public final class ClassAdapterTest {
}
};
try {
ClassAdapter.FACTORY.create(c.getClass(), NO_ANNOTATIONS, moshi);
ClassJsonAdapter.FACTORY.create(c.getClass(), NO_ANNOTATIONS, moshi);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("cannot serialize anonymous class " + c.getClass().getName());
@@ -330,7 +330,7 @@ public final class ClassAdapterTest {
}
@Test public void interfaceNotSupported() throws Exception {
assertThat(ClassAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull();
assertThat(ClassJsonAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull();
}
static abstract class Abstract {
@@ -338,11 +338,11 @@ public final class ClassAdapterTest {
@Test public void abstractClassNotSupported() throws Exception {
try {
ClassAdapter.FACTORY.create(Abstract.class, NO_ANNOTATIONS, moshi);
ClassJsonAdapter.FACTORY.create(Abstract.class, NO_ANNOTATIONS, moshi);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("cannot serialize abstract class "
+ "com.squareup.moshi.ClassAdapterTest$Abstract");
+ "com.squareup.moshi.ClassJsonAdapterTest$Abstract");
}
}
@@ -390,7 +390,7 @@ public final class ClassAdapterTest {
private <T> String toJson(Class<T> type, T value) throws IOException {
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassAdapter.FACTORY.create(
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(
type, NO_ANNOTATIONS, moshi);
// Wrap in an array to avoid top-level object warnings without going completely lenient.
@@ -408,7 +408,7 @@ public final class ClassAdapterTest {
private <T> T fromJson(Class<T> type, String json) throws IOException {
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassAdapter.FACTORY.create(
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(
type, NO_ANNOTATIONS, moshi);
// Wrap in an array to avoid top-level object warnings without going completely lenient.
JsonReader jsonReader = new JsonReader("[" + json + "]");

View File

@@ -0,0 +1,129 @@
/*
* Copyright (C) 2015 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.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import okio.Buffer;
import org.assertj.core.data.MapEntry;
import org.junit.Test;
import static com.squareup.moshi.Util.NO_ANNOTATIONS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
public final class MapJsonAdapterTest {
private final Moshi moshi = new Moshi.Builder().build();
@Test public void map() throws Exception {
Map<String, Boolean> map = new LinkedHashMap<>();
map.put("a", true);
map.put("b", false);
map.put("c", null);
String toJson = toJson(String.class, Boolean.class, map);
assertThat(toJson).isEqualTo("{\"a\":true,\"b\":false,\"c\":null}");
Map<String, Boolean> fromJson = fromJson(
String.class, Boolean.class, "{\"a\":true,\"b\":false,\"c\":null}");
assertThat(fromJson).containsExactly(
MapEntry.entry("a", true), MapEntry.entry("b", false), MapEntry.entry("c", null));
}
@Test public void mapWithNullKeyFailsToEmit() throws Exception {
Map<String, Boolean> map = new LinkedHashMap<>();
map.put(null, true);
try {
toJson(String.class, Boolean.class, map);
fail();
} catch (NullPointerException expected) {
}
}
@Test public void emptyMap() throws Exception {
Map<String, Boolean> map = new LinkedHashMap<>();
String toJson = toJson(String.class, Boolean.class, map);
assertThat(toJson).isEqualTo("{}");
Map<String, Boolean> fromJson = fromJson(String.class, Boolean.class, "{}");
assertThat(fromJson).isEmpty();
}
@Test public void nullMap() throws Exception {
JsonAdapter<?> jsonAdapter = mapAdapter(String.class, Boolean.class);
Buffer buffer = new Buffer();
JsonWriter jsonWriter = new JsonWriter(buffer);
jsonWriter.setLenient(true);
jsonAdapter.toJson(jsonWriter, null);
assertThat(buffer.readUtf8()).isEqualTo("null");
JsonReader jsonReader = new JsonReader("null");
jsonReader.setLenient(true);
assertThat(jsonAdapter.fromJson(jsonReader)).isEqualTo(null);
}
@Test public void orderIsRetained() throws Exception {
Map<String, Integer> map = new LinkedHashMap<>();
map.put("c", 1);
map.put("a", 2);
map.put("d", 3);
map.put("b", 4);
String toJson = toJson(String.class, Integer.class, map);
assertThat(toJson).isEqualTo("{\"c\":1,\"a\":2,\"d\":3,\"b\":4}");
Map<String, Integer> fromJson = fromJson(
String.class, Integer.class, "{\"c\":1,\"a\":2,\"d\":3,\"b\":4}");
assertThat(new ArrayList<Object>(fromJson.keySet()))
.isEqualTo(Arrays.asList("c", "a", "d", "b"));
}
@Test public void duplicatesAreForbidden() throws Exception {
try {
fromJson(String.class, Integer.class, "{\"c\":1,\"c\":2}");
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("object property 'c' has multiple values");
}
}
private <K, V> String toJson(Type keyType, Type valueType, Map<K, V> value) throws IOException {
JsonAdapter<Map<K, V>> jsonAdapter = mapAdapter(keyType, valueType);
Buffer buffer = new Buffer();
JsonWriter jsonWriter = new JsonWriter(buffer);
jsonWriter.setSerializeNulls(true);
jsonAdapter.toJson(jsonWriter, value);
return buffer.readUtf8();
}
@SuppressWarnings("unchecked") // It's the caller's responsibility to make sure K and V match.
private <K, V> JsonAdapter<Map<K, V>> mapAdapter(Type keyType, Type valueType) {
return (JsonAdapter<Map<K, V>>) MapJsonAdapter.FACTORY.create(
Types.newParameterizedType(Map.class, keyType, valueType), NO_ANNOTATIONS, moshi);
}
private <K, V> Map<K, V> fromJson(Type keyType, Type valueType, String json) throws IOException {
JsonAdapter<Map<K, V>> mapJsonAdapter = mapAdapter(keyType, valueType);
return mapJsonAdapter.fromJson(json);
}
}