Merge pull request #707 from square/jwilson.1009.streaming_RuntimeJsonAdapterFactory

Change RuntimeJsonAdapterFactory to peek for type names.
This commit is contained in:
Jesse Wilson
2018-10-13 20:43:53 -04:00
committed by GitHub
2 changed files with 101 additions and 85 deletions

View File

@@ -24,7 +24,10 @@ import com.squareup.moshi.Types;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.annotation.CheckReturnValue; import javax.annotation.CheckReturnValue;
@@ -39,8 +42,8 @@ import javax.annotation.CheckReturnValue;
* *
* Moshi moshi = new Moshi.Builder() * Moshi moshi = new Moshi.Builder()
* .add(RuntimeJsonAdapterFactory.of(Message.class, "type") * .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
* .registerSubtype(Success.class, "success") * .withSubtype(Success.class, "success")
* .registerSubtype(Error.class, "error")) * .withSubtype(Error.class, "error"))
* .build(); * .build();
* }</pre> * }</pre>
*/ */
@@ -48,7 +51,16 @@ import javax.annotation.CheckReturnValue;
final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory { final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
final Class<T> baseType; final Class<T> baseType;
final String labelKey; final String labelKey;
final Map<String, Type> labelToType = new LinkedHashMap<>(); final List<String> labels;
final List<Type> subtypes;
RuntimeJsonAdapterFactory(
Class<T> baseType, String labelKey, List<String> labels, List<Type> subtypes) {
this.baseType = baseType;
this.labelKey = labelKey;
this.labels = labels;
this.subtypes = subtypes;
}
/** /**
* @param baseType The base type for which this factory will create adapters. Cannot be Object. * @param baseType The base type for which this factory will create adapters. Cannot be Object.
@@ -63,27 +75,26 @@ final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"The base type must not be Object. Consider using a marker interface."); "The base type must not be Object. Consider using a marker interface.");
} }
return new RuntimeJsonAdapterFactory<>(baseType, labelKey); return new RuntimeJsonAdapterFactory<>(
} baseType, labelKey, Collections.<String>emptyList(), Collections.<Type>emptyList());
RuntimeJsonAdapterFactory(Class<T> baseType, String labelKey) {
this.baseType = baseType;
this.labelKey = labelKey;
} }
/** /**
* Register the subtype that can be created based on the label. When an unknown type is found * Returns a new factory that decodes instances of {@code subtype}. When an unknown type is found
* during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label * during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label
* is found during decoding a {@linkplain JsonDataException} will be thrown. * is found during decoding a {@linkplain JsonDataException} will be thrown.
*/ */
public RuntimeJsonAdapterFactory<T> registerSubtype(Class<? extends T> subtype, String label) { public RuntimeJsonAdapterFactory<T> withSubtype(Class<? extends T> subtype, String label) {
if (subtype == null) throw new NullPointerException("subtype == null"); if (subtype == null) throw new NullPointerException("subtype == null");
if (label == null) throw new NullPointerException("label == null"); if (label == null) throw new NullPointerException("label == null");
if (labelToType.containsKey(label) || labelToType.containsValue(subtype)) { if (labels.contains(label) || subtypes.contains(subtype)) {
throw new IllegalArgumentException("Subtypes and labels must be unique."); throw new IllegalArgumentException("Subtypes and labels must be unique.");
} }
labelToType.put(label, subtype); List<String> newLabels = new ArrayList<>(labels);
return this; newLabels.add(label);
List<Type> newSubtypes = new ArrayList<>(subtypes);
newSubtypes.add(subtype);
return new RuntimeJsonAdapterFactory<>(baseType, labelKey, newLabels, newSubtypes);
} }
@Override @Override
@@ -91,84 +102,90 @@ final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
if (Types.getRawType(type) != baseType || !annotations.isEmpty()) { if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
return null; return null;
} }
int size = labelToType.size();
Map<String, JsonAdapter<Object>> labelToAdapter = new LinkedHashMap<>(size); List<JsonAdapter<Object>> jsonAdapters = new ArrayList<>();
Map<Type, String> typeToLabel = new LinkedHashMap<>(size); for (int i = 0, size = subtypes.size(); i < size; i++) {
for (Map.Entry<String, Type> entry : labelToType.entrySet()) { jsonAdapters.add(moshi.adapter(subtypes.get(i)));
String label = entry.getKey();
Type typeValue = entry.getValue();
typeToLabel.put(typeValue, label);
labelToAdapter.put(label, moshi.adapter(typeValue));
} }
JsonAdapter<Object> objectJsonAdapter = moshi.adapter(Object.class); JsonAdapter<Object> objectJsonAdapter = moshi.adapter(Object.class);
return new RuntimeJsonAdapter(labelKey, labelToAdapter, typeToLabel, return new RuntimeJsonAdapter(
objectJsonAdapter).nullSafe(); labelKey, labels, subtypes, jsonAdapters, objectJsonAdapter).nullSafe();
} }
static final class RuntimeJsonAdapter extends JsonAdapter<Object> { static final class RuntimeJsonAdapter extends JsonAdapter<Object> {
final String labelKey; final String labelKey;
final Map<String, JsonAdapter<Object>> labelToAdapter; final List<String> labels;
final Map<Type, String> typeToLabel; final List<Type> subtypes;
final List<JsonAdapter<Object>> jsonAdapters;
final JsonAdapter<Object> objectJsonAdapter; final JsonAdapter<Object> objectJsonAdapter;
RuntimeJsonAdapter(String labelKey, Map<String, JsonAdapter<Object>> labelToAdapter, /** Single-element options containing the label's key only. */
Map<Type, String> typeToLabel, JsonAdapter<Object> objectJsonAdapter) { final JsonReader.Options labelKeyOptions;
/** Corresponds to subtypes. */
final JsonReader.Options labelOptions;
RuntimeJsonAdapter(String labelKey, List<String> labels,
List<Type> subtypes, List<JsonAdapter<Object>> jsonAdapters,
JsonAdapter<Object> objectJsonAdapter) {
this.labelKey = labelKey; this.labelKey = labelKey;
this.labelToAdapter = labelToAdapter; this.labels = labels;
this.typeToLabel = typeToLabel; this.subtypes = subtypes;
this.jsonAdapters = jsonAdapters;
this.objectJsonAdapter = objectJsonAdapter; this.objectJsonAdapter = objectJsonAdapter;
this.labelKeyOptions = JsonReader.Options.of(labelKey);
this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0]));
} }
@Override public Object fromJson(JsonReader reader) throws IOException { @Override public Object fromJson(JsonReader reader) throws IOException {
JsonReader.Token peekedToken = reader.peek(); int labelIndex = labelIndex(reader.peekJson());
if (peekedToken != JsonReader.Token.BEGIN_OBJECT) { return jsonAdapters.get(labelIndex).fromJson(reader);
throw new JsonDataException("Expected BEGIN_OBJECT but was " + peekedToken }
+ " at path " + reader.getPath());
private int labelIndex(JsonReader reader) throws IOException {
reader.beginObject();
while (reader.hasNext()) {
if (reader.selectName(labelKeyOptions) == -1) {
reader.skipName();
reader.skipValue();
continue;
}
int labelIndex = reader.selectString(labelOptions);
if (labelIndex == -1) {
throw new JsonDataException("Expected one of "
+ labels
+ " for key '"
+ labelKey
+ "' but found '"
+ reader.nextString()
+ "'. Register a subtype for this label.");
}
reader.close();
return labelIndex;
} }
Object jsonValue = reader.readJsonValue();
Map<String, Object> jsonObject = (Map<String, Object>) jsonValue; throw new JsonDataException("Missing label for " + labelKey);
Object label = jsonObject.get(labelKey);
if (label == null) {
throw new JsonDataException("Missing label for " + labelKey);
}
if (!(label instanceof String)) {
throw new JsonDataException("Label for '"
+ labelKey
+ "' must be a string but was "
+ label
+ ", a "
+ label.getClass());
}
JsonAdapter<Object> adapter = labelToAdapter.get(label);
if (adapter == null) {
throw new JsonDataException("Expected one of "
+ labelToAdapter.keySet()
+ " for key '"
+ labelKey
+ "' but found '"
+ label
+ "'. Register a subtype for this label.");
}
return adapter.fromJsonValue(jsonValue);
} }
@Override public void toJson(JsonWriter writer, Object value) throws IOException { @Override public void toJson(JsonWriter writer, Object value) throws IOException {
Class<?> type = value.getClass(); Class<?> type = value.getClass();
String label = typeToLabel.get(type); int labelIndex = subtypes.indexOf(type);
if (label == null) { if (labelIndex == -1) {
throw new IllegalArgumentException("Expected one of " throw new IllegalArgumentException("Expected one of "
+ typeToLabel.keySet() + subtypes
+ " but found " + " but found "
+ value + value
+ ", a " + ", a "
+ value.getClass() + value.getClass()
+ ". Register this subtype."); + ". Register this subtype.");
} }
JsonAdapter<Object> adapter = labelToAdapter.get(label); JsonAdapter<Object> adapter = jsonAdapters.get(labelIndex);
Map<String, Object> jsonValue = (Map<String, Object>) adapter.toJsonValue(value); Map<String, Object> jsonValue = (Map<String, Object>) adapter.toJsonValue(value);
Map<String, Object> valueWithLabel = new LinkedHashMap<>(1 + jsonValue.size()); Map<String, Object> valueWithLabel = new LinkedHashMap<>(1 + jsonValue.size());
valueWithLabel.put(labelKey, label); valueWithLabel.put(labelKey, labels.get(labelIndex));
valueWithLabel.putAll(jsonValue); valueWithLabel.putAll(jsonValue);
objectJsonAdapter.toJson(writer, valueWithLabel); objectJsonAdapter.toJson(writer, valueWithLabel);
} }

View File

@@ -33,8 +33,8 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void fromJson() throws IOException { @Test public void fromJson() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.registerSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.registerSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class); JsonAdapter<Message> adapter = moshi.adapter(Message.class);
@@ -47,8 +47,8 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void toJson() { @Test public void toJson() {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.registerSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.registerSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class); JsonAdapter<Message> adapter = moshi.adapter(Message.class);
@@ -61,8 +61,8 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void unregisteredLabelValue() throws IOException { @Test public void unregisteredLabelValue() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.registerSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.registerSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class); JsonAdapter<Message> adapter = moshi.adapter(Message.class);
@@ -75,14 +75,14 @@ public final class RuntimeJsonAdapterFactoryTest {
assertThat(expected).hasMessage("Expected one of [success, error] for key 'type' but found" assertThat(expected).hasMessage("Expected one of [success, error] for key 'type' but found"
+ " 'data'. Register a subtype for this label."); + " 'data'. Register a subtype for this label.");
} }
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT);
} }
@Test public void unregisteredSubtype() { @Test public void unregisteredSubtype() {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.registerSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.registerSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class); JsonAdapter<Message> adapter = moshi.adapter(Message.class);
@@ -101,8 +101,8 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void nonStringLabelValue() throws IOException { @Test public void nonStringLabelValue() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.registerSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.registerSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class); JsonAdapter<Message> adapter = moshi.adapter(Message.class);
@@ -110,16 +110,15 @@ public final class RuntimeJsonAdapterFactoryTest {
adapter.fromJson("{\"type\":{},\"value\":\"Okay!\"}"); adapter.fromJson("{\"type\":{},\"value\":\"Okay!\"}");
fail(); fail();
} catch (JsonDataException expected) { } catch (JsonDataException expected) {
assertThat(expected).hasMessage("Label for 'type' must be a string but was {}," assertThat(expected).hasMessage("Expected a string but was BEGIN_OBJECT at path $.type");
+ " a class com.squareup.moshi.LinkedHashTreeMap");
} }
} }
@Test public void nonObjectDoesNotConsume() throws IOException { @Test public void nonObjectDoesNotConsume() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.registerSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.registerSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class); JsonAdapter<Message> adapter = moshi.adapter(Message.class);
@@ -137,9 +136,9 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void uniqueSubtypes() { @Test public void uniqueSubtypes() {
RuntimeJsonAdapterFactory<Message> factory = RuntimeJsonAdapterFactory<Message> factory =
RuntimeJsonAdapterFactory.of(Message.class, "type") RuntimeJsonAdapterFactory.of(Message.class, "type")
.registerSubtype(Success.class, "success"); .withSubtype(Success.class, "success");
try { try {
factory.registerSubtype(Success.class, "data"); factory.withSubtype(Success.class, "data");
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Subtypes and labels must be unique."); assertThat(expected).hasMessage("Subtypes and labels must be unique.");
@@ -149,9 +148,9 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void uniqueLabels() { @Test public void uniqueLabels() {
RuntimeJsonAdapterFactory<Message> factory = RuntimeJsonAdapterFactory<Message> factory =
RuntimeJsonAdapterFactory.of(Message.class, "type") RuntimeJsonAdapterFactory.of(Message.class, "type")
.registerSubtype(Success.class, "data"); .withSubtype(Success.class, "data");
try { try {
factory.registerSubtype(Error.class, "data"); factory.withSubtype(Error.class, "data");
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Subtypes and labels must be unique."); assertThat(expected).hasMessage("Subtypes and labels must be unique.");
@@ -161,8 +160,8 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void nullSafe() throws IOException { @Test public void nullSafe() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.registerSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.registerSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class); JsonAdapter<Message> adapter = moshi.adapter(Message.class);