Mholloway has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/314428

Change subject: WIP: Add field checking for API responses
......................................................................

WIP: Add field checking for API responses

Provides a mechanism for enforcing the API contract on the client side.

Specifically, this provides a @RequiredFields annotation that can be used
to mark classes for field checking during deserialization.  When a class
is marked with @RequiredFields, a custom Gson type adapter will be created
that will check to ensure that fields designated as required are present
in the response, and return NULL if they are absent (rather than a
malformed object).

This is WIP but I welcome early reviews.  Specifically, I'm still on the
fence about using a class-level annotation with a list of required field
names passed in, as opposed to directly annotating the required fields.
The class-level annotation handling is rather more convenient to
implement and *maybe* in principle more performant, but is probably
clunkier from a consumer perspective than just annotating the fields.

TODO: Add tests.

Change-Id: Ic4e9d62cb24cb453d1c34e0618ff83479a924fe9
---
A 
app/src/main/java/org/wikipedia/dataclient/retrofit/FieldCheckingTypeAdapterFactory.java
A 
app/src/main/java/org/wikipedia/dataclient/retrofit/annotations/RequiredFields.java
M app/src/main/java/org/wikipedia/feed/image/FeaturedImage.java
M app/src/main/java/org/wikipedia/feed/model/CardPageItem.java
M app/src/main/java/org/wikipedia/feed/model/Thumbnail.java
M app/src/main/java/org/wikipedia/feed/mostread/MostReadArticle.java
M app/src/main/java/org/wikipedia/feed/mostread/MostReadArticles.java
M app/src/main/java/org/wikipedia/feed/news/NewsItem.java
M app/src/main/java/org/wikipedia/json/GsonUtil.java
9 files changed, 112 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/apps/android/wikipedia 
refs/changes/28/314428/1

diff --git 
a/app/src/main/java/org/wikipedia/dataclient/retrofit/FieldCheckingTypeAdapterFactory.java
 
b/app/src/main/java/org/wikipedia/dataclient/retrofit/FieldCheckingTypeAdapterFactory.java
new file mode 100644
index 0000000..b7f82ac
--- /dev/null
+++ 
b/app/src/main/java/org/wikipedia/dataclient/retrofit/FieldCheckingTypeAdapterFactory.java
@@ -0,0 +1,70 @@
+package org.wikipedia.dataclient.retrofit;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+import org.wikipedia.dataclient.retrofit.annotations.RequiredFields;
+import org.wikipedia.util.log.L;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
+
+// Standing on the shoulders of giants: https://stackoverflow.com/a/11272452
+public class FieldCheckingTypeAdapterFactory<C> implements TypeAdapterFactory {
+    @SuppressWarnings("unchecked") //use a runtime check to guarantee that 'C' 
and 'T' are equal
+    @Nullable
+    @Override
+    public final <T> TypeAdapter<T> create(@NonNull Gson gson, @NonNull 
TypeToken<T> typeToken) {
+        return typeToken.getRawType().getAnnotation(RequiredFields.class) != 
null
+                ? (TypeAdapter<T>) customizeClassAdapter(gson, (TypeToken<C>) 
typeToken)
+                : null;
+    }
+
+    @NonNull
+    private TypeAdapter<C> customizeClassAdapter(@NonNull Gson gson, @NonNull 
TypeToken<C> type) {
+        final TypeAdapter<C> delegate = gson.getDelegateAdapter(this, type);
+        final TypeAdapter<JsonElement> elementAdapter = 
gson.getAdapter(JsonElement.class);
+        return new TypeAdapter<C>() {
+            @Override public void write(JsonWriter out, C value) throws 
IOException {
+                JsonElement tree = delegate.toJsonTree(value);
+                elementAdapter.write(out, tree);
+            }
+            @Override @Nullable public C read(JsonReader in) throws 
IOException {
+                JsonElement tree = elementAdapter.read(in);
+                C deserialized = delegate.fromJsonTree(tree);
+                return hasRequiredFields(deserialized) ? 
delegate.fromJsonTree(tree) : null;
+            }
+        };
+    }
+
+    private boolean hasRequiredFields(@NonNull C deserialized) {
+        Field[] fields = deserialized.getClass().getDeclaredFields();
+        for (Field field : fields) {
+            //if (field.getAnnotation(Required.class) != null) {
+            List<String> requiredFields = Arrays.asList(deserialized.getClass()
+                    .getAnnotation(RequiredFields.class).value());
+            if (requiredFields.contains(field.getName())) {
+                try {
+                    field.setAccessible(true);
+                    if (field.get(deserialized) == null) {
+                        L.e("Missing field in JSON: " + field.getName());
+                        return false;
+                    }
+                } catch (IllegalArgumentException | IllegalAccessException 
throwable) {
+                    L.e(throwable);
+                }
+            }
+        }
+        return true;
+    }
+}
diff --git 
a/app/src/main/java/org/wikipedia/dataclient/retrofit/annotations/RequiredFields.java
 
b/app/src/main/java/org/wikipedia/dataclient/retrofit/annotations/RequiredFields.java
new file mode 100644
index 0000000..045663e
--- /dev/null
+++ 
b/app/src/main/java/org/wikipedia/dataclient/retrofit/annotations/RequiredFields.java
@@ -0,0 +1,24 @@
+package org.wikipedia.dataclient.retrofit.annotations;
+
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+
+/**
+ * Annotate Retrofit POJO classes with this and pass in a list of required 
fields to enforce their
+ * presence in order to return an instantiated object.
+ *
+ * E.g.: @RequiredFields({"title", "normalizedtitle"})
+ *
+ * Retrieve them with Class.getAnnotation(RequiredFields.class).value()
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(TYPE)
+public @interface RequiredFields {
+    String[] value();
+}
diff --git a/app/src/main/java/org/wikipedia/feed/image/FeaturedImage.java 
b/app/src/main/java/org/wikipedia/feed/image/FeaturedImage.java
index fcf26ce..c598b8a 100644
--- a/app/src/main/java/org/wikipedia/feed/image/FeaturedImage.java
+++ b/app/src/main/java/org/wikipedia/feed/image/FeaturedImage.java
@@ -3,8 +3,10 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
+import org.wikipedia.dataclient.retrofit.annotations.RequiredFields;
 import org.wikipedia.feed.model.Thumbnail;
 
+@RequiredFields({"title", "thumbnail", "image"})
 public final class FeaturedImage {
     @SuppressWarnings("unused,NullableProblems") @NonNull private String title;
     @SuppressWarnings("unused,NullableProblems") @NonNull private Thumbnail 
thumbnail;
@@ -43,6 +45,7 @@
      * returns the translation for the request Site language, if available.  
Otherwise it defaults
      * to providing the English translation.
      */
+    @RequiredFields({"text", "lang"})
     private static class Description {
         @SuppressWarnings("unused,NullableProblems") @NonNull private String 
text;
         @SuppressWarnings("unused,NullableProblems") @NonNull private String 
lang;
diff --git a/app/src/main/java/org/wikipedia/feed/model/CardPageItem.java 
b/app/src/main/java/org/wikipedia/feed/model/CardPageItem.java
index 7a3e0b6..43fbcc5 100644
--- a/app/src/main/java/org/wikipedia/feed/model/CardPageItem.java
+++ b/app/src/main/java/org/wikipedia/feed/model/CardPageItem.java
@@ -6,10 +6,12 @@
 
 import com.google.gson.annotations.JsonAdapter;
 
+import org.wikipedia.dataclient.retrofit.annotations.RequiredFields;
 import org.wikipedia.page.Namespace;
 import org.wikipedia.page.NamespaceTypeAdapter;
 import org.wikipedia.util.StringUtil;
 
+@RequiredFields({"title", "normalizedtitle"})
 public final class CardPageItem {
     @SuppressWarnings("unused,NullableProblems") @NonNull private String title;
     @SuppressWarnings("unused,NullableProblems") @NonNull private String 
normalizedtitle;
diff --git a/app/src/main/java/org/wikipedia/feed/model/Thumbnail.java 
b/app/src/main/java/org/wikipedia/feed/model/Thumbnail.java
index 11ad000..984d662 100644
--- a/app/src/main/java/org/wikipedia/feed/model/Thumbnail.java
+++ b/app/src/main/java/org/wikipedia/feed/model/Thumbnail.java
@@ -3,6 +3,9 @@
 import android.net.Uri;
 import android.support.annotation.NonNull;
 
+import org.wikipedia.dataclient.retrofit.annotations.RequiredFields;
+
+@RequiredFields({"source"})
 public final class Thumbnail {
     @SuppressWarnings("unused,NullableProblems") @NonNull private Uri source;
     @SuppressWarnings("unused") private int height;
diff --git a/app/src/main/java/org/wikipedia/feed/mostread/MostReadArticle.java 
b/app/src/main/java/org/wikipedia/feed/mostread/MostReadArticle.java
index 29358d8..9ab34b3 100644
--- a/app/src/main/java/org/wikipedia/feed/mostread/MostReadArticle.java
+++ b/app/src/main/java/org/wikipedia/feed/mostread/MostReadArticle.java
@@ -6,8 +6,10 @@
 
 import com.google.gson.annotations.SerializedName;
 
+import org.wikipedia.dataclient.retrofit.annotations.RequiredFields;
 import org.wikipedia.feed.model.Thumbnail;
 
+@RequiredFields({"title", "normalizedtitle"})
 public final class MostReadArticle {
     @SerializedName("normalizedtitle") 
@SuppressWarnings("unused,NullableProblems") @NonNull private String 
normalizedTitle;
     @SuppressWarnings("unused,NullableProblems") @NonNull private String title;
diff --git 
a/app/src/main/java/org/wikipedia/feed/mostread/MostReadArticles.java 
b/app/src/main/java/org/wikipedia/feed/mostread/MostReadArticles.java
index cb1e7e3..a6d7f10 100644
--- a/app/src/main/java/org/wikipedia/feed/mostread/MostReadArticles.java
+++ b/app/src/main/java/org/wikipedia/feed/mostread/MostReadArticles.java
@@ -2,9 +2,12 @@
 
 import android.support.annotation.NonNull;
 
+import org.wikipedia.dataclient.retrofit.annotations.RequiredFields;
+
 import java.util.Date;
 import java.util.List;
 
+@RequiredFields({"date", "articles"})
 public final class MostReadArticles {
     @SuppressWarnings("unused,NullableProblems") @NonNull private Date date;
     @SuppressWarnings("unused,NullableProblems") @NonNull private 
List<MostReadArticle> articles;
diff --git a/app/src/main/java/org/wikipedia/feed/news/NewsItem.java 
b/app/src/main/java/org/wikipedia/feed/news/NewsItem.java
index 3980099..6fb58a1 100644
--- a/app/src/main/java/org/wikipedia/feed/news/NewsItem.java
+++ b/app/src/main/java/org/wikipedia/feed/news/NewsItem.java
@@ -6,6 +6,7 @@
 
 import org.wikipedia.Constants;
 import org.wikipedia.Site;
+import org.wikipedia.dataclient.retrofit.annotations.RequiredFields;
 import org.wikipedia.feed.model.CardPageItem;
 import org.wikipedia.news.NewsLinkCard;
 
@@ -14,6 +15,7 @@
 
 import static org.wikipedia.util.ImageUrlUtil.getUrlForSize;
 
+@RequiredFields({"story", "links"})
 public final class NewsItem {
     @SuppressWarnings("unused,NullableProblems") @NonNull private String story;
     @SuppressWarnings("unused,NullableProblems") @NonNull private 
List<CardPageItem> links;
diff --git a/app/src/main/java/org/wikipedia/json/GsonUtil.java 
b/app/src/main/java/org/wikipedia/json/GsonUtil.java
index fd9f2a9..0a74ff1 100644
--- a/app/src/main/java/org/wikipedia/json/GsonUtil.java
+++ b/app/src/main/java/org/wikipedia/json/GsonUtil.java
@@ -5,10 +5,13 @@
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 
+import org.wikipedia.dataclient.retrofit.FieldCheckingTypeAdapterFactory;
+
 public final class GsonUtil {
     private static final String DATE_FORMAT = "MMM dd, yyyy HH:mm:ss";
     private static final Gson DEFAULT_GSON = new GsonBuilder()
             .setDateFormat(DATE_FORMAT)
+            .registerTypeAdapterFactory(new 
FieldCheckingTypeAdapterFactory<>())
             .registerTypeAdapter(Uri.class, new UriTypeAdapter())
             .create();
 

-- 
To view, visit https://gerrit.wikimedia.org/r/314428
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ic4e9d62cb24cb453d1c34e0618ff83479a924fe9
Gerrit-PatchSet: 1
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Mholloway <mhollo...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to