mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09:21 +08:00
Merge pull request #707 from square/jwilson.1009.streaming_RuntimeJsonAdapterFactory
Change RuntimeJsonAdapterFactory to peek for type names.
This commit is contained in:
@@ -24,7 +24,10 @@ import com.squareup.moshi.Types;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
@@ -39,8 +42,8 @@ import javax.annotation.CheckReturnValue;
|
||||
*
|
||||
* Moshi moshi = new Moshi.Builder()
|
||||
* .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
* .registerSubtype(Success.class, "success")
|
||||
* .registerSubtype(Error.class, "error"))
|
||||
* .withSubtype(Success.class, "success")
|
||||
* .withSubtype(Error.class, "error"))
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
@@ -48,7 +51,16 @@ import javax.annotation.CheckReturnValue;
|
||||
final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
|
||||
final Class<T> baseType;
|
||||
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.
|
||||
@@ -63,27 +75,26 @@ final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
|
||||
throw new IllegalArgumentException(
|
||||
"The base type must not be Object. Consider using a marker interface.");
|
||||
}
|
||||
return new RuntimeJsonAdapterFactory<>(baseType, labelKey);
|
||||
}
|
||||
|
||||
RuntimeJsonAdapterFactory(Class<T> baseType, String labelKey) {
|
||||
this.baseType = baseType;
|
||||
this.labelKey = labelKey;
|
||||
return new RuntimeJsonAdapterFactory<>(
|
||||
baseType, labelKey, Collections.<String>emptyList(), Collections.<Type>emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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 (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.");
|
||||
}
|
||||
labelToType.put(label, subtype);
|
||||
return this;
|
||||
List<String> newLabels = new ArrayList<>(labels);
|
||||
newLabels.add(label);
|
||||
List<Type> newSubtypes = new ArrayList<>(subtypes);
|
||||
newSubtypes.add(subtype);
|
||||
return new RuntimeJsonAdapterFactory<>(baseType, labelKey, newLabels, newSubtypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,84 +102,90 @@ final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
|
||||
if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int size = labelToType.size();
|
||||
Map<String, JsonAdapter<Object>> labelToAdapter = new LinkedHashMap<>(size);
|
||||
Map<Type, String> typeToLabel = new LinkedHashMap<>(size);
|
||||
for (Map.Entry<String, Type> entry : labelToType.entrySet()) {
|
||||
String label = entry.getKey();
|
||||
Type typeValue = entry.getValue();
|
||||
typeToLabel.put(typeValue, label);
|
||||
labelToAdapter.put(label, moshi.adapter(typeValue));
|
||||
|
||||
List<JsonAdapter<Object>> jsonAdapters = new ArrayList<>();
|
||||
for (int i = 0, size = subtypes.size(); i < size; i++) {
|
||||
jsonAdapters.add(moshi.adapter(subtypes.get(i)));
|
||||
}
|
||||
|
||||
JsonAdapter<Object> objectJsonAdapter = moshi.adapter(Object.class);
|
||||
return new RuntimeJsonAdapter(labelKey, labelToAdapter, typeToLabel,
|
||||
objectJsonAdapter).nullSafe();
|
||||
return new RuntimeJsonAdapter(
|
||||
labelKey, labels, subtypes, jsonAdapters, objectJsonAdapter).nullSafe();
|
||||
}
|
||||
|
||||
static final class RuntimeJsonAdapter extends JsonAdapter<Object> {
|
||||
final String labelKey;
|
||||
final Map<String, JsonAdapter<Object>> labelToAdapter;
|
||||
final Map<Type, String> typeToLabel;
|
||||
final List<String> labels;
|
||||
final List<Type> subtypes;
|
||||
final List<JsonAdapter<Object>> jsonAdapters;
|
||||
final JsonAdapter<Object> objectJsonAdapter;
|
||||
|
||||
RuntimeJsonAdapter(String labelKey, Map<String, JsonAdapter<Object>> labelToAdapter,
|
||||
Map<Type, String> typeToLabel, JsonAdapter<Object> objectJsonAdapter) {
|
||||
/** Single-element options containing the label's key only. */
|
||||
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.labelToAdapter = labelToAdapter;
|
||||
this.typeToLabel = typeToLabel;
|
||||
this.labels = labels;
|
||||
this.subtypes = subtypes;
|
||||
this.jsonAdapters = jsonAdapters;
|
||||
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 {
|
||||
JsonReader.Token peekedToken = reader.peek();
|
||||
if (peekedToken != JsonReader.Token.BEGIN_OBJECT) {
|
||||
throw new JsonDataException("Expected BEGIN_OBJECT but was " + peekedToken
|
||||
+ " at path " + reader.getPath());
|
||||
int labelIndex = labelIndex(reader.peekJson());
|
||||
return jsonAdapters.get(labelIndex).fromJson(reader);
|
||||
}
|
||||
Object jsonValue = reader.readJsonValue();
|
||||
Map<String, Object> jsonObject = (Map<String, Object>) jsonValue;
|
||||
Object label = jsonObject.get(labelKey);
|
||||
if (label == null) {
|
||||
throw new JsonDataException("Missing label for " + labelKey);
|
||||
|
||||
private int labelIndex(JsonReader reader) throws IOException {
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
if (reader.selectName(labelKeyOptions) == -1) {
|
||||
reader.skipName();
|
||||
reader.skipValue();
|
||||
continue;
|
||||
}
|
||||
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) {
|
||||
|
||||
int labelIndex = reader.selectString(labelOptions);
|
||||
if (labelIndex == -1) {
|
||||
throw new JsonDataException("Expected one of "
|
||||
+ labelToAdapter.keySet()
|
||||
+ labels
|
||||
+ " for key '"
|
||||
+ labelKey
|
||||
+ "' but found '"
|
||||
+ label
|
||||
+ reader.nextString()
|
||||
+ "'. Register a subtype for this label.");
|
||||
}
|
||||
return adapter.fromJsonValue(jsonValue);
|
||||
reader.close();
|
||||
return labelIndex;
|
||||
}
|
||||
|
||||
throw new JsonDataException("Missing label for " + labelKey);
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
Class<?> type = value.getClass();
|
||||
String label = typeToLabel.get(type);
|
||||
if (label == null) {
|
||||
int labelIndex = subtypes.indexOf(type);
|
||||
if (labelIndex == -1) {
|
||||
throw new IllegalArgumentException("Expected one of "
|
||||
+ typeToLabel.keySet()
|
||||
+ subtypes
|
||||
+ " but found "
|
||||
+ value
|
||||
+ ", a "
|
||||
+ value.getClass()
|
||||
+ ". 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> valueWithLabel = new LinkedHashMap<>(1 + jsonValue.size());
|
||||
valueWithLabel.put(labelKey, label);
|
||||
valueWithLabel.put(labelKey, labels.get(labelIndex));
|
||||
valueWithLabel.putAll(jsonValue);
|
||||
objectJsonAdapter.toJson(writer, valueWithLabel);
|
||||
}
|
||||
|
@@ -33,8 +33,8 @@ public final class RuntimeJsonAdapterFactoryTest {
|
||||
@Test public void fromJson() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(Error.class, "error"))
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
@@ -47,8 +47,8 @@ public final class RuntimeJsonAdapterFactoryTest {
|
||||
@Test public void toJson() {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(Error.class, "error"))
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
@@ -61,8 +61,8 @@ public final class RuntimeJsonAdapterFactoryTest {
|
||||
@Test public void unregisteredLabelValue() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(Error.class, "error"))
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
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"
|
||||
+ " '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() {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(Error.class, "error"))
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
@@ -101,8 +101,8 @@ public final class RuntimeJsonAdapterFactoryTest {
|
||||
@Test public void nonStringLabelValue() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(Error.class, "error"))
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
@@ -110,16 +110,15 @@ public final class RuntimeJsonAdapterFactoryTest {
|
||||
adapter.fromJson("{\"type\":{},\"value\":\"Okay!\"}");
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected).hasMessage("Label for 'type' must be a string but was {},"
|
||||
+ " a class com.squareup.moshi.LinkedHashTreeMap");
|
||||
assertThat(expected).hasMessage("Expected a string but was BEGIN_OBJECT at path $.type");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void nonObjectDoesNotConsume() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(Error.class, "error"))
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
@@ -137,9 +136,9 @@ public final class RuntimeJsonAdapterFactoryTest {
|
||||
@Test public void uniqueSubtypes() {
|
||||
RuntimeJsonAdapterFactory<Message> factory =
|
||||
RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success");
|
||||
.withSubtype(Success.class, "success");
|
||||
try {
|
||||
factory.registerSubtype(Success.class, "data");
|
||||
factory.withSubtype(Success.class, "data");
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("Subtypes and labels must be unique.");
|
||||
@@ -149,9 +148,9 @@ public final class RuntimeJsonAdapterFactoryTest {
|
||||
@Test public void uniqueLabels() {
|
||||
RuntimeJsonAdapterFactory<Message> factory =
|
||||
RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "data");
|
||||
.withSubtype(Success.class, "data");
|
||||
try {
|
||||
factory.registerSubtype(Error.class, "data");
|
||||
factory.withSubtype(Error.class, "data");
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("Subtypes and labels must be unique.");
|
||||
@@ -161,8 +160,8 @@ public final class RuntimeJsonAdapterFactoryTest {
|
||||
@Test public void nullSafe() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(Error.class, "error"))
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
|
Reference in New Issue
Block a user