diff --git a/src/main/java/io/split/android/client/fallback/FallbackConfiguration.java b/src/main/java/io/split/android/client/fallback/FallbackConfiguration.java new file mode 100644 index 000000000..0ba19fd56 --- /dev/null +++ b/src/main/java/io/split/android/client/fallback/FallbackConfiguration.java @@ -0,0 +1,103 @@ +package io.split.android.client.fallback; + +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public final class FallbackConfiguration { + + @Nullable + private final FallbackTreatment mGlobal; + private final Map mByFlag; + + private FallbackConfiguration(@Nullable FallbackTreatment global, + @Nullable Map byFlag) { + mGlobal = global; + if (byFlag == null || byFlag.isEmpty()) { + mByFlag = Collections.emptyMap(); + } else { + mByFlag = Collections.unmodifiableMap(new HashMap<>(byFlag)); + } + } + + @Nullable + public FallbackTreatment getGlobal() { + return mGlobal; + } + + public Map getByFlag() { + return mByFlag; + } + + /** + * Creates a new {@link Builder} for {@link FallbackConfiguration}. + * Use this to provide an optional global fallback and flag-specific fallbacks. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + @Nullable + private FallbackTreatment mGlobal; + @Nullable + private Map mByFlag; + + private Builder() { + mGlobal = null; + mByFlag = null; + } + + /** + * Sets an optional global fallback treatment to be used when no flag-specific + * fallback exists for a given flag. This value is returned only in place of + * the "control" treatment. + * + * @param global optional global {@link FallbackTreatment} + * @return this builder instance + */ + public Builder global(@Nullable FallbackTreatment global) { + mGlobal = global; + return this; + } + + /** + * Sets optional flag-specific fallback treatments, where keys are flag names. + * These take precedence over the global fallback. + * + * @param byFlag map of flag name to {@link FallbackTreatment}; may be null or empty + * @return this builder instance + */ + public Builder byFlag(@Nullable Map byFlag) { + mByFlag = byFlag; + return this; + } + + /** + * Builds an immutable {@link FallbackConfiguration} snapshot of the + * configured values. + * + * @return a new immutable {@link FallbackConfiguration} + */ + public FallbackConfiguration build() { + return new FallbackConfiguration(mGlobal, mByFlag); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FallbackConfiguration that = (FallbackConfiguration) o; + return Objects.equals(mGlobal, that.mGlobal) && + Objects.equals(mByFlag, that.mByFlag); + } + + @Override + public int hashCode() { + return Objects.hash(mGlobal, mByFlag); + } +} diff --git a/src/main/java/io/split/android/client/fallback/FallbackTreatment.java b/src/main/java/io/split/android/client/fallback/FallbackTreatment.java new file mode 100644 index 000000000..bf969e4d1 --- /dev/null +++ b/src/main/java/io/split/android/client/fallback/FallbackTreatment.java @@ -0,0 +1,55 @@ +package io.split.android.client.fallback; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +/** + * Represents the fallback treatment, with an optional config and a fixed label. + */ +public final class FallbackTreatment { + + public static final String LABEL = "fallback treatment"; + + @NonNull + private final String mTreatment; + @Nullable + private final String mConfig; + + public FallbackTreatment(@NonNull String treatment) { + this(treatment, null); + } + + public FallbackTreatment(@NonNull String treatment, @Nullable String config) { + mTreatment = treatment; + mConfig = config; + } + + public String getTreatment() { + return mTreatment; + } + + @Nullable + public String getConfig() { + return mConfig; + } + + public String getLabel() { + return LABEL; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FallbackTreatment that = (FallbackTreatment) o; + return Objects.equals(mTreatment, that.mTreatment) && + Objects.equals(mConfig, that.mConfig); + } + + @Override + public int hashCode() { + return Objects.hash(mTreatment, mConfig); + } +} diff --git a/src/test/java/io/split/android/client/fallback/FallbackConfigurationTest.java b/src/test/java/io/split/android/client/fallback/FallbackConfigurationTest.java new file mode 100644 index 000000000..ae786c583 --- /dev/null +++ b/src/test/java/io/split/android/client/fallback/FallbackConfigurationTest.java @@ -0,0 +1,72 @@ +package io.split.android.client.fallback; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class FallbackConfigurationTest { + + @Test + public void constructorSetsFields() { + FallbackTreatment global = new FallbackTreatment("off"); + Map map = new HashMap<>(); + map.put("flagA", new FallbackTreatment("off")); + + FallbackConfiguration cfg = FallbackConfiguration.builder() + .global(global) + .byFlag(map) + .build(); + + assertSame(global, cfg.getGlobal()); + assertEquals(1, cfg.getByFlag().size()); + assertEquals("off", cfg.getByFlag().get("flagA").getTreatment()); + } + + @Test + public void byFlagIsUnmodifiable() { + FallbackTreatment global = new FallbackTreatment("off"); + Map byFlag = new HashMap<>(); + byFlag.put("flagA", new FallbackTreatment("off")); + + FallbackConfiguration config = FallbackConfiguration.builder() + .global(global) + .byFlag(byFlag) + .build(); + + byFlag.put("flagB", new FallbackTreatment("on")); + + // config map must not change + assertEquals(1, config.getByFlag().size()); + + try { + config.getByFlag().put("x", new FallbackTreatment("on")); + throw new AssertionError("Map should be unmodifiable"); + } catch (UnsupportedOperationException expected) { + + } + } + + @Test + public void equalityAndHashCodeByValue() { + FallbackTreatment global = new FallbackTreatment("off"); + Map a = new HashMap<>(); + a.put("flagA", new FallbackTreatment("off")); + + Map b = new HashMap<>(); + b.put("flagA", new FallbackTreatment("off")); + + FallbackConfiguration configOne = FallbackConfiguration.builder().global(global).byFlag(a).build(); + FallbackConfiguration configTwo = FallbackConfiguration.builder().global(global).byFlag(b).build(); + FallbackConfiguration configThree = FallbackConfiguration.builder().global(null).byFlag(b).build(); + + assertEquals(configOne, configTwo); + assertEquals(configOne.hashCode(), configTwo.hashCode()); + assertNotEquals(configOne, configThree); + assertNotEquals(configOne.hashCode(), configThree.hashCode()); + } +} diff --git a/src/test/java/io/split/android/client/fallback/FallbackTreatmentTest.java b/src/test/java/io/split/android/client/fallback/FallbackTreatmentTest.java new file mode 100644 index 000000000..e59127b3a --- /dev/null +++ b/src/test/java/io/split/android/client/fallback/FallbackTreatmentTest.java @@ -0,0 +1,47 @@ +package io.split.android.client.fallback; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +public class FallbackTreatmentTest { + + private static final String FALLBACK_TREATMENT = "fallback treatment"; + + @Test + public void constructorSetsFields() { + FallbackTreatment ft = new FallbackTreatment("off", "{\"k\":true}"); + assertEquals("off", ft.getTreatment()); + assertEquals("{\"k\":true}", ft.getConfig()); + assertEquals(FALLBACK_TREATMENT, ft.getLabel()); + } + + @Test + public void configCanBeNull() { + FallbackTreatment ft = new FallbackTreatment("off", null); + assertEquals("off", ft.getTreatment()); + assertNull(ft.getConfig()); + assertEquals(FALLBACK_TREATMENT, ft.getLabel()); + } + + @Test + public void convenienceConstructorSetsNullConfig() { + FallbackTreatment ft = new FallbackTreatment("off"); + assertEquals("off", ft.getTreatment()); + assertNull(ft.getConfig()); + assertEquals(FALLBACK_TREATMENT, ft.getLabel()); + } + + @Test + public void equalityAndHashCodeByValue() { + FallbackTreatment a = new FallbackTreatment("off", null); + FallbackTreatment b = new FallbackTreatment("off", null); + FallbackTreatment c = new FallbackTreatment("on", null); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertNotEquals(a, c); + } +}