Merge pull request #713 from square/jwilson.1019.runtime_polymorphic

Rename RuntimeJsonAdapterFactory to PolymorphicJsonAdapterFactory
This commit is contained in:
Jesse Wilson 2018-10-20 00:54:14 -04:00 committed by GitHub
commit daa0441ab3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 91 additions and 35 deletions

View file

@ -31,28 +31,84 @@ import java.util.Set;
import javax.annotation.CheckReturnValue;
/**
* A JsonAdapter factory for polymorphic types. This is useful when the type is not known before
* decoding the JSON. This factory's adapters expect JSON in the format of a JSON object with a
* key whose value is a label that determines the type to which to map the JSON object. To use, add
* this factory to your {@link Moshi.Builder}:
* A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON
* Moshi uses this type information to determine which class to decode to. When encoding Moshi uses
* the objects class to determine what type information to include.
*
* <p>Suppose we have an interface, its implementations, and a class that uses them:
*
* <pre> {@code
*
* interface HandOfCards {
* }
*
* class BlackjackHand extends HandOfCards {
* Card hidden_card;
* List<Card> visible_cards;
* }
*
* class HoldemHand extends HandOfCards {
* Set<Card> hidden_cards;
* }
*
* class Player {
* String name;
* HandOfCards hand;
* }
* }</pre>
*
* <p>We want to decode the following JSON into the player model above:
*
* <pre> {@code
*
* {
* "name": "Jesse",
* "hand": {
* "hand_type": "blackjack",
* "hidden_card": "9D",
* "visible_cards": ["8H", "4C"]
* }
* }
* }</pre>
*
* <p>Left unconfigured, Moshi would incorrectly attempt to decode the hand object to the abstract
* {@code HandOfCards} interface. We configure it to use the appropriate subtype instead:
*
* <pre> {@code
*
* Moshi moshi = new Moshi.Builder()
* .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
* .withSubtype(Success.class, "success")
* .withSubtype(Error.class, "error"))
* .add(PolymorphicJsonAdapterFactory.of(HandOfCards.class, "hand_type")
* .withSubtype(BlackjackHand.class, "blackjack")
* .withSubtype(HoldemHand.class, "holdem"))
* .build();
* }</pre>
*
* <p>This class imposes strict requirements on its use:
*
* <ul>
* <li>Base types may be classes or interfaces. You may not use {@code Object.class} as a base
* type.
* <li>Subtypes must encode as JSON objects.
* <li>Type information must be in the encoded object. Each message must have a type label like
* {@code hand_type} whose value is a string like {@code blackjack} that identifies which type
* to use.
* <li>Each type identifier must be unique.
* </ul>
*
* <p>For best performance type information should be the first field in the object. Otherwise Moshi
* must reprocess the JSON stream once it knows the object's type.
*
* <p>If an unknown subtype is encountered when decoding, this will throw a {@link
* JsonDataException}. If an unknown type is encountered when encoding, this will throw an {@link
* IllegalArgumentException}.
*/
public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Factory {
final Class<T> baseType;
final String labelKey;
final List<String> labels;
final List<Type> subtypes;
RuntimeJsonAdapterFactory(
PolymorphicJsonAdapterFactory(
Class<T> baseType, String labelKey, List<String> labels, List<Type> subtypes) {
this.baseType = baseType;
this.labelKey = labelKey;
@ -66,14 +122,14 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
* JSON object.
*/
@CheckReturnValue
public static <T> RuntimeJsonAdapterFactory<T> of(Class<T> baseType, String labelKey) {
public static <T> PolymorphicJsonAdapterFactory<T> of(Class<T> baseType, String labelKey) {
if (baseType == null) throw new NullPointerException("baseType == null");
if (labelKey == null) throw new NullPointerException("labelKey == null");
if (baseType == Object.class) {
throw new IllegalArgumentException(
"The base type must not be Object. Consider using a marker interface.");
}
return new RuntimeJsonAdapterFactory<>(
return new PolymorphicJsonAdapterFactory<>(
baseType, labelKey, Collections.<String>emptyList(), Collections.<Type>emptyList());
}
@ -82,7 +138,7 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
* 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> withSubtype(Class<? extends T> subtype, String label) {
public PolymorphicJsonAdapterFactory<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 (labels.contains(label) || subtypes.contains(subtype)) {
@ -92,7 +148,7 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
newLabels.add(label);
List<Type> newSubtypes = new ArrayList<>(subtypes);
newSubtypes.add(subtype);
return new RuntimeJsonAdapterFactory<>(baseType, labelKey, newLabels, newSubtypes);
return new PolymorphicJsonAdapterFactory<>(baseType, labelKey, newLabels, newSubtypes);
}
@Override
@ -107,11 +163,11 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
}
JsonAdapter<Object> objectJsonAdapter = moshi.adapter(Object.class);
return new RuntimeJsonAdapter(
return new PolymorphicJsonAdapter(
labelKey, labels, subtypes, jsonAdapters, objectJsonAdapter).nullSafe();
}
static final class RuntimeJsonAdapter extends JsonAdapter<Object> {
static final class PolymorphicJsonAdapter extends JsonAdapter<Object> {
final String labelKey;
final List<String> labels;
final List<Type> subtypes;
@ -123,7 +179,7 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
/** Corresponds to subtypes. */
final JsonReader.Options labelOptions;
RuntimeJsonAdapter(String labelKey, List<String> labels,
PolymorphicJsonAdapter(String labelKey, List<String> labels,
List<Type> subtypes, List<JsonAdapter<Object>> jsonAdapters,
JsonAdapter<Object> objectJsonAdapter) {
this.labelKey = labelKey;
@ -189,7 +245,7 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
}
@Override public String toString() {
return "RuntimeJsonAdapter(" + labelKey + ")";
return "PolymorphicJsonAdapter(" + labelKey + ")";
}
}
}

View file

@ -29,10 +29,10 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
@SuppressWarnings("CheckReturnValue")
public final class RuntimeJsonAdapterFactoryTest {
public final class PolymorphicJsonAdapterFactoryTest {
@Test public void fromJson() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
@ -46,7 +46,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void toJson() {
Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
@ -60,7 +60,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void unregisteredLabelValue() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
@ -80,7 +80,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void unregisteredSubtype() {
Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
@ -90,17 +90,17 @@ public final class RuntimeJsonAdapterFactoryTest {
adapter.toJson(new EmptyMessage());
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Expected one of [class"
+ " com.squareup.moshi.adapters.RuntimeJsonAdapterFactoryTest$Success, class"
+ " com.squareup.moshi.adapters.RuntimeJsonAdapterFactoryTest$Error] but found"
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Success, class"
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Error] but found"
+ " EmptyMessage, a class"
+ " com.squareup.moshi.adapters.RuntimeJsonAdapterFactoryTest$EmptyMessage. Register"
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$EmptyMessage. Register"
+ " this subtype.");
}
}
@Test public void nonStringLabelValue() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
@ -116,7 +116,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void nonObjectDoesNotConsume() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
@ -134,8 +134,8 @@ public final class RuntimeJsonAdapterFactoryTest {
}
@Test public void uniqueSubtypes() {
RuntimeJsonAdapterFactory<Message> factory =
RuntimeJsonAdapterFactory.of(Message.class, "type")
PolymorphicJsonAdapterFactory<Message> factory =
PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success");
try {
factory.withSubtype(Success.class, "data");
@ -146,8 +146,8 @@ public final class RuntimeJsonAdapterFactoryTest {
}
@Test public void uniqueLabels() {
RuntimeJsonAdapterFactory<Message> factory =
RuntimeJsonAdapterFactory.of(Message.class, "type")
PolymorphicJsonAdapterFactory<Message> factory =
PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "data");
try {
factory.withSubtype(Error.class, "data");
@ -159,7 +159,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void nullSafe() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
@ -172,7 +172,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void disallowObjectBaseType() {
try {
RuntimeJsonAdapterFactory.of(Object.class, "type");
PolymorphicJsonAdapterFactory.of(Object.class, "type");
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage(
@ -186,7 +186,7 @@ public final class RuntimeJsonAdapterFactoryTest {
*/
@Test public void unportableTypes() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(MessageWithUnportableTypes.class, "unportable"))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);