Repository: firebase/firebase-admin-java
Pull request #489 (2020-10-16 19:55:05)

Add Remote Config conditions to template
- Add Remote Config Conditions
- Add unit tests

Related Issue: #446 

lahirumaramba: Hi @egilmorez! Please take a look at the docs when you get a chance. Thank you!

Patch:
diff --git a/src/main/java/com/google/firebase/remoteconfig/Condition.java b/src/main/java/com/google/firebase/remoteconfig/Condition.java
new file mode 100644
index 0000000..c10498c
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/Condition.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.google.firebase.remoteconfig;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Strings;
+import com.google.firebase.internal.NonNull;
+import com.google.firebase.internal.Nullable;
+import com.google.firebase.remoteconfig.internal.TemplateResponse.ConditionResponse;
+
+import java.util.Objects;
+
+/**
+ * Represents a Remote Config condition that can be included in a {@link Template}.
+ * A condition targets a specific group of users. A list of these conditions make up
+ * part of a Remote Config template.
+ */
+public final class Condition {
+
+  private String name;
+  private String expression;
+  private TagColor tagColor;
+
+  /**
+   * Creates a new {@link Condition}.
+   *
+   * @param name A non-null, non-empty, and unique name of this condition.
+   * @param expression A non-null and non-empty expression of this condition.
+   */
+  public Condition(@NonNull String name, @NonNull String expression) {
+    this(name, expression, null);
+  }
+
+  /**
+   * Creates a new {@link Condition}.
+   *
+   * @param name A non-null, non-empty, and unique name of this condition.
+   * @param expression A non-null and non-empty expression of this condition.
+   * @param tagColor A color associated with this condition for display purposes in the
+   *                 Firebase Console. Not specifying this value results in the console picking an
+   *                 arbitrary color to associate with the condition.
+   */
+  public Condition(@NonNull String name, @NonNull String expression, @Nullable TagColor tagColor) {
+    checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty");
+    checkArgument(!Strings.isNullOrEmpty(expression),
+            "condition expression must not be null or empty");
+    this.name = name;
+    this.expression = expression;
+    this.tagColor = tagColor;
+  }
+
+  Condition(@NonNull ConditionResponse conditionResponse) {
+    checkNotNull(conditionResponse);
+    this.name = conditionResponse.getName();
+    this.expression = conditionResponse.getExpression();
+    if (conditionResponse.getTagColor() == null) {
+      this.tagColor = TagColor.UNSPECIFIED;
+    } else {
+      this.tagColor = TagColor.valueOf(conditionResponse.getTagColor());
+    }
+  }
+
+  /**
+   * Gets the name of the condition.
+   *
+   * @return The name of the condition.
+   */
+  @NonNull
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Gets the expression of the condition.
+   *
+   * @return The expression of the condition.
+   */
+  @NonNull
+  public String getExpression() {
+    return expression;
+  }
+
+  /**
+   * Gets the tag color of the condition used for display purposes in the Firebase Console.
+   *
+   * @return The tag color of the condition.
+   */
+  @NonNull
+  public TagColor getTagColor() {
+    return tagColor;
+  }
+
+  /**
+   * Sets the name of the condition.
+   *
+   * @param name A non-empty and unique name of this condition.
+   * @return This {@link Condition}.
+   */
+  public Condition setName(@NonNull String name) {
+    checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty");
+    this.name = name;
+    return this;
+  }
+
+  /**
+   * Sets the expression of the condition.
+   *
+   * <p>See <a href="https://firebase.google.com/docs/remote-config/condition-reference">
+   * condition expressions</a> for the expected syntax of this field.
+   *
+   * @param expression The logic of this condition.
+   * @return This {@link Condition}.
+   */
+  public Condition setExpression(@NonNull String expression) {
+    checkArgument(!Strings.isNullOrEmpty(expression),
+            "condition expression must not be null or empty");
+    this.expression = expression;
+    return this;
+  }
+
+  /**
+   * Sets the tag color of the condition.
+   *
+   * <p>The color associated with this condition for display purposes in the Firebase Console.
+   * Not specifying this value results in the console picking an arbitrary color to associate
+   * with the condition.
+   *
+   * @param tagColor The tag color of this condition.
+   * @return This {@link Condition}.
+   */
+  public Condition setTagColor(@Nullable TagColor tagColor) {
+    this.tagColor = tagColor;
+    return this;
+  }
+
+  ConditionResponse toConditionResponse() {
+    return new ConditionResponse()
+            .setName(this.name)
+            .setExpression(this.expression)
+            .setTagColor(this.tagColor == null ? null : this.tagColor.getColor());
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    Condition condition = (Condition) o;
+    return Objects.equals(name, condition.name)
+            && Objects.equals(expression, condition.expression) && tagColor == condition.tagColor;
+  }
+}
diff --git a/src/main/java/com/google/firebase/remoteconfig/TagColor.java b/src/main/java/com/google/firebase/remoteconfig/TagColor.java
new file mode 100644
index 0000000..ed7b8cd
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/TagColor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.google.firebase.remoteconfig;
+
+/**
+ * Colors that are associated with conditions for display purposes in the Firebase Console.
+ */
+public enum TagColor {
+  BLUE("BLUE"),
+  BROWN("BROWN"),
+  CYAN("CYAN"),
+  DEEP_ORANGE("DEEP_ORANGE"),
+  GREEN("GREEN"),
+  INDIGO("INDIGO"),
+  LIME("LIME"),
+  ORANGE("ORANGE"),
+  PINK("PINK"),
+  PURPLE("PURPLE"),
+  TEAL("TEAL"),
+  UNSPECIFIED("CONDITION_DISPLAY_COLOR_UNSPECIFIED");
+
+  private final String color;
+
+  TagColor(String color) {
+    this.color = color;
+  }
+
+  public String getColor() {
+    return color;
+  }
+}
diff --git a/src/main/java/com/google/firebase/remoteconfig/Template.java b/src/main/java/com/google/firebase/remoteconfig/Template.java
index 53e5b60..902273a 100644
--- a/src/main/java/com/google/firebase/remoteconfig/Template.java
+++ b/src/main/java/com/google/firebase/remoteconfig/Template.java
@@ -21,7 +21,9 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.remoteconfig.internal.TemplateResponse;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -31,23 +33,32 @@ public final class Template {
 
   private String etag;
   private Map<String, Parameter> parameters;
+  private List<Condition> conditions;
 
   /**
    * Creates a new {@link Template}.
    */
   public Template() {
     parameters = new HashMap<>();
+    conditions = new ArrayList<>();
   }
 
   Template(@NonNull TemplateResponse templateResponse) {
     checkNotNull(templateResponse);
     this.parameters = new HashMap<>();
+    this.conditions = new ArrayList<>();
     if (templateResponse.getParameters() != null) {
       for (Map.Entry<String, TemplateResponse.ParameterResponse> entry
               : templateResponse.getParameters().entrySet()) {
         this.parameters.put(entry.getKey(), new Parameter(entry.getValue()));
       }
     }
+    if (templateResponse.getConditions() != null) {
+      for (TemplateResponse.ConditionResponse conditionResponse
+              : templateResponse.getConditions()) {
+        this.conditions.add(new Condition(conditionResponse));
+      }
+    }
   }
 
   /**
@@ -70,6 +81,16 @@ public final class Template {
     return this.parameters;
   }
 
+  /**
+   * Gets the list of conditions of the template.
+   *
+   * @return A non-null list of conditions
+   */
+  @NonNull
+  public List<Condition> getConditions() {
+    return conditions;
+  }
+
   /**
    * Sets the map of parameters of the template.
    *
@@ -84,6 +105,19 @@ public final class Template {
     return this;
   }
 
+  /**
+   * Sets the list of conditions of the template.
+   *
+   * @param conditions A non-null list of conditions in descending order by priority.
+   * @return This {@link Template} instance.
+   */
+  public Template setConditions(
+          @NonNull List<Condition> conditions) {
+    checkNotNull(conditions, "conditions must not be null.");
+    this.conditions = conditions;
+    return this;
+  }
+
   Template setETag(String etag) {
     this.etag = etag;
     return this;
@@ -91,9 +125,15 @@ public final class Template {
 
   TemplateResponse toTemplateResponse() {
     Map<String, TemplateResponse.ParameterResponse> parameterResponses = new HashMap<>();
-    for (Map.Entry<String, Parameter> entry : parameters.entrySet()) {
+    for (Map.Entry<String, Parameter> entry : this.parameters.entrySet()) {
       parameterResponses.put(entry.getKey(), entry.getValue().toParameterResponse());
     }
-    return new TemplateResponse().setParameters(parameterResponses);
+    List<TemplateResponse.ConditionResponse> conditionResponses = new ArrayList<>();
+    for (Condition condition : this.conditions) {
+      conditionResponses.add(condition.toConditionResponse());
+    }
+    return new TemplateResponse()
+            .setParameters(parameterResponses)
+            .setConditions(conditionResponses);
   }
 }
diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java
index e36b458..cd1463e 100644
--- a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java
+++ b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java
@@ -18,6 +18,7 @@ package com.google.firebase.remoteconfig.internal;
 
 import com.google.api.client.util.Key;
 
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -29,16 +30,29 @@ public final class TemplateResponse {
   @Key("parameters")
   private Map<String, ParameterResponse> parameters;
 
+  @Key("conditions")
+  private List<ConditionResponse> conditions;
+
   public Map<String, ParameterResponse> getParameters() {
     return parameters;
   }
 
+  public List<ConditionResponse> getConditions() {
+    return conditions;
+  }
+
   public TemplateResponse setParameters(
           Map<String, ParameterResponse> parameters) {
     this.parameters = parameters;
     return this;
   }
 
+  public TemplateResponse setConditions(
+          List<ConditionResponse> conditions) {
+    this.conditions = conditions;
+    return this;
+  }
+
   /**
    * The Data Transfer Object for parsing Remote Config parameter responses from the
    * Remote Config service.
@@ -114,4 +128,47 @@ public final class TemplateResponse {
       return this;
     }
   }
+
+  /**
+   * The Data Transfer Object for parsing Remote Config condition responses from the
+   * Remote Config service.
+   **/
+  public static final class ConditionResponse {
+
+    @Key("name")
+    private String name;
+
+    @Key("expression")
+    private String expression;
+
+    @Key("tagColor")
+    private String tagColor;
+
+    public String getName() {
+      return name;
+    }
+
+    public String getExpression() {
+      return expression;
+    }
+
+    public String getTagColor() {
+      return tagColor;
+    }
+
+    public ConditionResponse setName(String name) {
+      this.name = name;
+      return this;
+    }
+
+    public ConditionResponse setExpression(String expression) {
+      this.expression = expression;
+      return this;
+    }
+
+    public ConditionResponse setTagColor(String tagColor) {
+      this.tagColor = tagColor;
+      return this;
+    }
+  }
 }


Test patch:
diff --git a/src/test/java/com/google/firebase/remoteconfig/ConditionTest.java b/src/test/java/com/google/firebase/remoteconfig/ConditionTest.java
new file mode 100644
index 0000000..1c8b8cd
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/ConditionTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.google.firebase.remoteconfig;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import org.junit.Test;
+
+public class ConditionTest {
+
+  @Test
+  public void testEquality() {
+    final Condition conditionOne = new Condition("ios", "device.os == 'ios'", TagColor.GREEN);
+    final Condition conditionTwo = new Condition("ios", "device.os == 'ios'", TagColor.GREEN);
+    final Condition conditionThree = new Condition("android", "device.os == 'android'");
+    final Condition conditionFour = new Condition("android", "device.os == 'android'");
+    final Condition conditionFive = new Condition("ios", "device.os == 'ios'", TagColor.BLUE);
+
+    assertEquals(conditionOne, conditionTwo);
+    assertEquals(conditionThree, conditionFour);
+    assertNotEquals(conditionOne, conditionThree);
+    assertNotEquals(conditionTwo, conditionFour);
+    assertNotEquals(conditionOne, conditionFive);
+  }
+}
diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
index d733ed7..aabc98a 100644
--- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
+++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
@@ -86,6 +86,7 @@ public class FirebaseRemoteConfigClientImplTest {
 
     Template template = client.getTemplate();
 
+    // Check Parameters
     assertEquals(TEST_ETAG, template.getETag());
     Map<String, Parameter> parameters = template.getParameters();
     assertEquals(2, parameters.size());
@@ -107,6 +108,20 @@ public class FirebaseRemoteConfigClientImplTest {
     assertTrue(
             headerParameter.getDefaultValue() instanceof ParameterValue.InAppDefault);
     checkGetRequestHeader(interceptor.getLastRequest());
+
+    // Check Conditions
+    List<Condition> actualConditions = template.getConditions();
+    List<Condition> expectedConditions = ImmutableList.of(
+            new Condition("ios_en", "device.os == 'ios' && device.country in ['us', 'uk']")
+                    .setTagColor(TagColor.INDIGO),
+            new Condition("android_en",
+                    "device.os == 'android' && device.country in ['us', 'uk']")
+                    .setTagColor(TagColor.UNSPECIFIED)
+    );
+    assertEquals(expectedConditions.size(), actualConditions.size());
+    for (int i = 0; i < expectedConditions.size(); i++) {
+      assertEquals(expectedConditions.get(i), actualConditions.get(i));
+    }
   }
 
   @Test
diff --git a/src/test/resources/getRemoteConfig.json b/src/test/resources/getRemoteConfig.json
index 86322f2..df54117 100644
--- a/src/test/resources/getRemoteConfig.json
+++ b/src/test/resources/getRemoteConfig.json
@@ -7,8 +7,7 @@
     },
     {
       "name": "android_en",
-      "expression": "device.os == 'android' && device.country in ['us', 'uk']",
-      "tagColor": "GREEN"
+      "expression": "device.os == 'android' && device.country in ['us', 'uk']"
     }
   ],
   "parameters": {