From 6926b0fe48841fff913836350bc306e931a14ac2 Mon Sep 17 00:00:00 2001 From: Samuele Maci <macisamuele@gmail.com> Date: Tue, 4 Feb 2020 21:39:58 +0100 Subject: [PATCH] Include PolymorphicJsonAdapterFactory from https://github.com/square/moshi/pull/1094/ --- ...olymorphicJsonAdapterFactory.java.mustache | 344 ++++++++++++++++++ .../tools/PolymorphicJsonAdapterFactory.java | 338 +++++++++++++++++ 2 files changed, 682 insertions(+) create mode 100644 gradle-plugin/plugin/src/main/resources/kotlin/tools/PolymorphicJsonAdapterFactory.java.mustache create mode 100644 samples/junit-tests/src/main/java/com/yelp/codegen/generatecodesamples/tools/PolymorphicJsonAdapterFactory.java diff --git a/gradle-plugin/plugin/src/main/resources/kotlin/tools/PolymorphicJsonAdapterFactory.java.mustache b/gradle-plugin/plugin/src/main/resources/kotlin/tools/PolymorphicJsonAdapterFactory.java.mustache new file mode 100644 index 00000000..0b8a73a8 --- /dev/null +++ b/gradle-plugin/plugin/src/main/resources/kotlin/tools/PolymorphicJsonAdapterFactory.java.mustache @@ -0,0 +1,344 @@ +/** + * This file has been extracted from https://github.com/square/moshi/pull/1094/files. + * + * <p>Once the PR is merged and a new moshi version is released this file should be removed from the + * repo. + * + * <p>Copyright (C) 2011 Google Inc. + * + * <p>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 + * + * <p>http://www.apache.org/licenses/LICENSE-2.0 + * + * <p>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 {{packageName}}.tools; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; +import com.squareup.moshi.Moshi; +import com.squareup.moshi.Types; +import io.reactivex.annotations.CheckReturnValue; // Different respect the version on the PR +import io.reactivex.annotations.Nullable; // Different respect the version on the PR +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.List; +import java.util.Set; + +/** + * 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 object’s 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(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. + * <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: + * + * <ul> + * <li>if {@link #withDefaultValue(Object)} is used, then {@code defaultValue} will be returned + * <li>if {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the {@code + * fallbackJsonAdapter.fromJson(reader)} result will be returned + * <li>otherwise a {@link JsonDataException} will be thrown + * </ul> + * + * <p>If an unknown type is encountered when encoding: + * + * <ul> + * <li>if {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the {@code + * fallbackJsonAdapter.toJson(writer, value)} result will be returned + * <li>otherwise a {@link IllegalArgumentException} will be thrown + * </ul> + * + * <p>If the same subtype has multiple labels the first one is used when encoding. + */ +public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Factory { + /** + * Thin wrapper around {@link JsonAdapter} to allow {@link PolymorphicJsonAdapter} to + * distinguish between {@code JsonAdapter} added due to a {@code defaultValue} or added by users + * of {@link PolymorphicJsonAdapterFactory}. + */ + private abstract static class DefaultJsonAdapter<T> extends JsonAdapter<T> {} + + final Class<T> baseType; + final String labelKey; + final List<String> labels; + final List<Type> subtypes; + @Nullable final JsonAdapter<Object> fallbackJsonAdapter; + + PolymorphicJsonAdapterFactory( + Class<T> baseType, + String labelKey, + List<String> labels, + List<Type> subtypes, + @Nullable JsonAdapter<Object> fallbackJsonAdapter) { + this.baseType = baseType; + this.labelKey = labelKey; + this.labels = labels; + this.subtypes = subtypes; + this.fallbackJsonAdapter = fallbackJsonAdapter; + } + + /** + * @param baseType The base type for which this factory will create adapters. Cannot be Object. + * @param labelKey The key in the JSON object whose value determines the type to which to map + * the JSON object. + */ + @CheckReturnValue + 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"); + return new PolymorphicJsonAdapterFactory<>( + baseType, + labelKey, + Collections.<String>emptyList(), + Collections.<Type>emptyList(), + null); + } + + /** Returns a new factory that decodes instances of {@code subtype}. */ + 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)) { + throw new IllegalArgumentException("Labels must be unique."); + } + List<String> newLabels = new ArrayList<>(labels); + newLabels.add(label); + List<Type> newSubtypes = new ArrayList<>(subtypes); + newSubtypes.add(subtype); + return new PolymorphicJsonAdapterFactory<>( + baseType, labelKey, newLabels, newSubtypes, fallbackJsonAdapter); + } + + /** + * Returns a new factory that with default to {@code fallbackJsonAdapter.fromJson(reader)} upon + * decoding of unrecognized labels. + * + * <p>The {@link JsonReader} instance will not be automatically consumed, so sure to consume it + * within your implementation of {@link JsonAdapter#fromJson(JsonReader)} + */ + public PolymorphicJsonAdapterFactory<T> withFallbackJsonAdapter( + @Nullable JsonAdapter<Object> fallbackJsonAdapter) { + return new PolymorphicJsonAdapterFactory<>( + baseType, labelKey, labels, subtypes, fallbackJsonAdapter); + } + + /** + * Returns a new factory that will default to {@code defaultValue} upon decoding of unrecognized + * labels. The default value should be immutable. + */ + public PolymorphicJsonAdapterFactory<T> withDefaultValue(@Nullable T defaultValue) { + return withFallbackJsonAdapter(buildFallbackJsonAdapter(defaultValue)); + } + + private JsonAdapter<Object> buildFallbackJsonAdapter(final T defaultValue) { + return new DefaultJsonAdapter<Object>() { + @Nullable + @Override + public Object fromJson(JsonReader reader) throws IOException { + reader.skipValue(); + return defaultValue; + } + + @Override + public void toJson(JsonWriter writer, @Nullable Object value) throws IOException { + throw new IOException( + "This method should never be called. " + + "If you find this on your stacktraces please report it " + + "to the https://github.com/square/moshi project"); + } + }; + } + + @Override + public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) { + if (Types.getRawType(type) != baseType || !annotations.isEmpty()) { + return null; + } + + List<JsonAdapter<Object>> jsonAdapters = new ArrayList<>(subtypes.size()); + for (int i = 0, size = subtypes.size(); i < size; i++) { + jsonAdapters.add(moshi.adapter(subtypes.get(i))); + } + + return new PolymorphicJsonAdapter( + labelKey, labels, subtypes, jsonAdapters, fallbackJsonAdapter) + .nullSafe(); + } + + static final class PolymorphicJsonAdapter extends JsonAdapter<Object> { + final String labelKey; + final List<String> labels; + final List<Type> subtypes; + final List<JsonAdapter<Object>> jsonAdapters; + @Nullable final JsonAdapter<Object> fallbackJsonAdapter; + + /** Single-element options containing the label's key only. */ + final JsonReader.Options labelKeyOptions; + /** Corresponds to subtypes. */ + final JsonReader.Options labelOptions; + + PolymorphicJsonAdapter( + String labelKey, + List<String> labels, + List<Type> subtypes, + List<JsonAdapter<Object>> jsonAdapters, + @Nullable JsonAdapter<Object> fallbackJsonAdapter) { + this.labelKey = labelKey; + this.labels = labels; + this.subtypes = subtypes; + this.jsonAdapters = jsonAdapters; + this.fallbackJsonAdapter = fallbackJsonAdapter; + + 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 peeked = reader.peekJson(); + peeked.setFailOnUnknown(false); + int labelIndex; + try { + labelIndex = labelIndex(peeked); + } finally { + peeked.close(); + } + if (labelIndex == -1) { + return this.fallbackJsonAdapter.fromJson(reader); + } else { + return jsonAdapters.get(labelIndex).fromJson(reader); + } + } + + 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 && this.fallbackJsonAdapter == null) { + throw new JsonDataException( + "Expected one of " + + labels + + " for key '" + + labelKey + + "' but found '" + + reader.nextString() + + "'. Register a subtype for this label."); + } + return labelIndex; + } + + throw new JsonDataException("Missing label for " + labelKey); + } + + @Override + public void toJson(JsonWriter writer, Object value) throws IOException { + Class<?> type = value.getClass(); + int labelIndex = subtypes.indexOf(type); + final JsonAdapter<Object> adapter; + if (labelIndex == -1) { + if (fallbackJsonAdapter == null + || fallbackJsonAdapter instanceof DefaultJsonAdapter) { + throw new IllegalArgumentException( + "Expected one of " + + subtypes + + " but found " + + value + + ", a " + + value.getClass() + + ". Register this subtype."); + } else { + adapter = fallbackJsonAdapter; + } + } else { + adapter = jsonAdapters.get(labelIndex); + } + + writer.beginObject(); + if (adapter != fallbackJsonAdapter) { + writer.name(labelKey).value(labels.get(labelIndex)); + } + int flattenToken = writer.beginFlatten(); + adapter.toJson(writer, value); + writer.endFlatten(flattenToken); + writer.endObject(); + } + + @Override + public String toString() { + return "PolymorphicJsonAdapter(" + labelKey + ")"; + } + } +} diff --git a/samples/junit-tests/src/main/java/com/yelp/codegen/generatecodesamples/tools/PolymorphicJsonAdapterFactory.java b/samples/junit-tests/src/main/java/com/yelp/codegen/generatecodesamples/tools/PolymorphicJsonAdapterFactory.java new file mode 100644 index 00000000..0e37557a --- /dev/null +++ b/samples/junit-tests/src/main/java/com/yelp/codegen/generatecodesamples/tools/PolymorphicJsonAdapterFactory.java @@ -0,0 +1,338 @@ +/** + * This file has been extracted from https://github.com/square/moshi/pull/1094/files. + * + * <p>Once the PR is merged and a new moshi version is released this file should be removed from the + * repo. + * + * <p>Copyright (C) 2011 Google Inc. + * + * <p>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 + * + * <p>http://www.apache.org/licenses/LICENSE-2.0 + * + * <p>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.yelp.codegen.generatecodesamples.tools; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; +import com.squareup.moshi.Moshi; +import com.squareup.moshi.Types; +import io.reactivex.annotations.CheckReturnValue; // Different respect the version on the PR +import io.reactivex.annotations.Nullable; // Different respect the version on the PR +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.List; +import java.util.Set; + +/** + * 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 object’s 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(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. + * <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: + * + * <ul> + * <li>if {@link #withDefaultValue(Object)} is used, then {@code defaultValue} will be returned + * <li>if {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the {@code + * fallbackJsonAdapter.fromJson(reader)} result will be returned + * <li>otherwise a {@link JsonDataException} will be thrown + * </ul> + * + * <p>If an unknown type is encountered when encoding: + * + * <ul> + * <li>if {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the {@code + * fallbackJsonAdapter.toJson(writer, value)} result will be returned + * <li>otherwise a {@link IllegalArgumentException} will be thrown + * </ul> + * + * <p>If the same subtype has multiple labels the first one is used when encoding. + */ +public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Factory { + /** + * Thin wrapper around {@link JsonAdapter} to allow {@link PolymorphicJsonAdapter} to distinguish + * between {@code JsonAdapter} added due to a {@code defaultValue} or added by users of {@link + * PolymorphicJsonAdapterFactory}. + */ + private abstract static class DefaultJsonAdapter<T> extends JsonAdapter<T> {} + + final Class<T> baseType; + final String labelKey; + final List<String> labels; + final List<Type> subtypes; + @Nullable final JsonAdapter<Object> fallbackJsonAdapter; + + PolymorphicJsonAdapterFactory( + Class<T> baseType, + String labelKey, + List<String> labels, + List<Type> subtypes, + @Nullable JsonAdapter<Object> fallbackJsonAdapter) { + this.baseType = baseType; + this.labelKey = labelKey; + this.labels = labels; + this.subtypes = subtypes; + this.fallbackJsonAdapter = fallbackJsonAdapter; + } + + /** + * @param baseType The base type for which this factory will create adapters. Cannot be Object. + * @param labelKey The key in the JSON object whose value determines the type to which to map the + * JSON object. + */ + @CheckReturnValue + 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"); + return new PolymorphicJsonAdapterFactory<>( + baseType, labelKey, Collections.<String>emptyList(), Collections.<Type>emptyList(), null); + } + + /** Returns a new factory that decodes instances of {@code subtype}. */ + 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)) { + throw new IllegalArgumentException("Labels must be unique."); + } + List<String> newLabels = new ArrayList<>(labels); + newLabels.add(label); + List<Type> newSubtypes = new ArrayList<>(subtypes); + newSubtypes.add(subtype); + return new PolymorphicJsonAdapterFactory<>( + baseType, labelKey, newLabels, newSubtypes, fallbackJsonAdapter); + } + + /** + * Returns a new factory that with default to {@code fallbackJsonAdapter.fromJson(reader)} upon + * decoding of unrecognized labels. + * + * <p>The {@link JsonReader} instance will not be automatically consumed, so sure to consume it + * within your implementation of {@link JsonAdapter#fromJson(JsonReader)} + */ + public PolymorphicJsonAdapterFactory<T> withFallbackJsonAdapter( + @Nullable JsonAdapter<Object> fallbackJsonAdapter) { + return new PolymorphicJsonAdapterFactory<>( + baseType, labelKey, labels, subtypes, fallbackJsonAdapter); + } + + /** + * Returns a new factory that will default to {@code defaultValue} upon decoding of unrecognized + * labels. The default value should be immutable. + */ + public PolymorphicJsonAdapterFactory<T> withDefaultValue(@Nullable T defaultValue) { + return withFallbackJsonAdapter(buildFallbackJsonAdapter(defaultValue)); + } + + private JsonAdapter<Object> buildFallbackJsonAdapter(final T defaultValue) { + return new DefaultJsonAdapter<Object>() { + @Nullable + @Override + public Object fromJson(JsonReader reader) throws IOException { + reader.skipValue(); + return defaultValue; + } + + @Override + public void toJson(JsonWriter writer, @Nullable Object value) throws IOException { + throw new IOException( + "This method should never be called. " + + "If you find this on your stacktraces please report it " + + "to the https://github.com/square/moshi project"); + } + }; + } + + @Override + public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) { + if (Types.getRawType(type) != baseType || !annotations.isEmpty()) { + return null; + } + + List<JsonAdapter<Object>> jsonAdapters = new ArrayList<>(subtypes.size()); + for (int i = 0, size = subtypes.size(); i < size; i++) { + jsonAdapters.add(moshi.adapter(subtypes.get(i))); + } + + return new PolymorphicJsonAdapter(labelKey, labels, subtypes, jsonAdapters, fallbackJsonAdapter) + .nullSafe(); + } + + static final class PolymorphicJsonAdapter extends JsonAdapter<Object> { + final String labelKey; + final List<String> labels; + final List<Type> subtypes; + final List<JsonAdapter<Object>> jsonAdapters; + @Nullable final JsonAdapter<Object> fallbackJsonAdapter; + + /** Single-element options containing the label's key only. */ + final JsonReader.Options labelKeyOptions; + /** Corresponds to subtypes. */ + final JsonReader.Options labelOptions; + + PolymorphicJsonAdapter( + String labelKey, + List<String> labels, + List<Type> subtypes, + List<JsonAdapter<Object>> jsonAdapters, + @Nullable JsonAdapter<Object> fallbackJsonAdapter) { + this.labelKey = labelKey; + this.labels = labels; + this.subtypes = subtypes; + this.jsonAdapters = jsonAdapters; + this.fallbackJsonAdapter = fallbackJsonAdapter; + + 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 peeked = reader.peekJson(); + peeked.setFailOnUnknown(false); + int labelIndex; + try { + labelIndex = labelIndex(peeked); + } finally { + peeked.close(); + } + if (labelIndex == -1) { + return this.fallbackJsonAdapter.fromJson(reader); + } else { + return jsonAdapters.get(labelIndex).fromJson(reader); + } + } + + 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 && this.fallbackJsonAdapter == null) { + throw new JsonDataException( + "Expected one of " + + labels + + " for key '" + + labelKey + + "' but found '" + + reader.nextString() + + "'. Register a subtype for this label."); + } + return labelIndex; + } + + throw new JsonDataException("Missing label for " + labelKey); + } + + @Override + public void toJson(JsonWriter writer, Object value) throws IOException { + Class<?> type = value.getClass(); + int labelIndex = subtypes.indexOf(type); + final JsonAdapter<Object> adapter; + if (labelIndex == -1) { + if (fallbackJsonAdapter == null || fallbackJsonAdapter instanceof DefaultJsonAdapter) { + throw new IllegalArgumentException( + "Expected one of " + + subtypes + + " but found " + + value + + ", a " + + value.getClass() + + ". Register this subtype."); + } else { + adapter = fallbackJsonAdapter; + } + } else { + adapter = jsonAdapters.get(labelIndex); + } + + writer.beginObject(); + if (adapter != fallbackJsonAdapter) { + writer.name(labelKey).value(labels.get(labelIndex)); + } + int flattenToken = writer.beginFlatten(); + adapter.toJson(writer, value); + writer.endFlatten(flattenToken); + writer.endObject(); + } + + @Override + public String toString() { + return "PolymorphicJsonAdapter(" + labelKey + ")"; + } + } +}