diff --git a/src/main/java/io/split/android/client/fallback/FallbackTreatment.java b/src/main/java/io/split/android/client/fallback/FallbackTreatment.java index bf969e4d1..f75f9f9c0 100644 --- a/src/main/java/io/split/android/client/fallback/FallbackTreatment.java +++ b/src/main/java/io/split/android/client/fallback/FallbackTreatment.java @@ -5,12 +5,19 @@ import java.util.Objects; +import io.split.android.grammar.Treatments; + /** * Represents the fallback treatment, with an optional config and a fixed label. */ public final class FallbackTreatment { - public static final String LABEL = "fallback treatment"; + private static final String LABEL_PREFIX = "fallback - "; + + /** + * Default fallback representing "control" treatment with no config. + */ + public static final FallbackTreatment CONTROL = new FallbackTreatment(Treatments.CONTROL); @NonNull private final String mTreatment; @@ -35,8 +42,8 @@ public String getConfig() { return mConfig; } - public String getLabel() { - return LABEL; + public String getLabelPrefix() { + return LABEL_PREFIX; } @Override diff --git a/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculator.java b/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculator.java new file mode 100644 index 000000000..cbcb18cac --- /dev/null +++ b/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculator.java @@ -0,0 +1,19 @@ +package io.split.android.client.fallback; + +import androidx.annotation.NonNull; + +/** + * Resolves a fallback treatment for a given flag name. + * Returns null if no fallback applies (caller should use control). + */ +public interface FallbackTreatmentsCalculator { + + /** + * Resolve a fallback for a given flag name. + * + * @param flagName non-null flag name + * @return a fallback treatment if configured, otherwise "control" + */ + @NonNull + FallbackTreatment resolve(@NonNull String flagName); +} diff --git a/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorImpl.java b/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorImpl.java new file mode 100644 index 000000000..08d2ee493 --- /dev/null +++ b/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorImpl.java @@ -0,0 +1,32 @@ +package io.split.android.client.fallback; + +import androidx.annotation.NonNull; + +import java.util.Map; + +public final class FallbackTreatmentsCalculatorImpl implements FallbackTreatmentsCalculator { + + @NonNull + private final FallbackConfiguration mConfig; + + public FallbackTreatmentsCalculatorImpl(@NonNull FallbackConfiguration config) { + mConfig = config; + } + + @NonNull + @Override + public FallbackTreatment resolve(@NonNull String flagName) { + Map byFlag = mConfig.getByFlag(); + if (byFlag != null) { + FallbackTreatment flagTreatment = byFlag.get(flagName); + if (flagTreatment != null) { + return flagTreatment; + } + } + FallbackTreatment global = mConfig.getGlobal(); + if (global != null) { + return global; + } + return FallbackTreatment.CONTROL; + } +} diff --git a/src/main/java/io/split/android/grammar/Treatments.java b/src/main/java/io/split/android/grammar/Treatments.java index 9d352b078..66c1e2b1d 100644 --- a/src/main/java/io/split/android/grammar/Treatments.java +++ b/src/main/java/io/split/android/grammar/Treatments.java @@ -7,25 +7,7 @@ public class Treatments { public static final String CONTROL = "control"; - - /** - * OFF is a synonym for CONTROL. - */ public static final String OFF = "off"; public static final String ON = "on"; - public static boolean isControl(String treatment) { - return CONTROL.equals(treatment) || OFF.equals(treatment); - } - - public static String controlSynonym(String treatment) { - if (!isControl(treatment)) { - throw new IllegalArgumentException("Not a control treatment: " + treatment); - } - if (Treatments.OFF.equals(treatment)) { - return Treatments.CONTROL; - } - return Treatments.OFF; - } - } diff --git a/src/test/java/io/split/android/client/fallback/FallbackTreatmentTest.java b/src/test/java/io/split/android/client/fallback/FallbackTreatmentTest.java index e59127b3a..a01dbb46a 100644 --- a/src/test/java/io/split/android/client/fallback/FallbackTreatmentTest.java +++ b/src/test/java/io/split/android/client/fallback/FallbackTreatmentTest.java @@ -8,14 +8,14 @@ public class FallbackTreatmentTest { - private static final String FALLBACK_TREATMENT = "fallback treatment"; + private static final String FALLBACK_TREATMENT = "fallback - "; @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()); + assertEquals(FALLBACK_TREATMENT, ft.getLabelPrefix()); } @Test @@ -23,7 +23,7 @@ public void configCanBeNull() { FallbackTreatment ft = new FallbackTreatment("off", null); assertEquals("off", ft.getTreatment()); assertNull(ft.getConfig()); - assertEquals(FALLBACK_TREATMENT, ft.getLabel()); + assertEquals(FALLBACK_TREATMENT, ft.getLabelPrefix()); } @Test @@ -31,7 +31,7 @@ public void convenienceConstructorSetsNullConfig() { FallbackTreatment ft = new FallbackTreatment("off"); assertEquals("off", ft.getTreatment()); assertNull(ft.getConfig()); - assertEquals(FALLBACK_TREATMENT, ft.getLabel()); + assertEquals(FALLBACK_TREATMENT, ft.getLabelPrefix()); } @Test diff --git a/src/test/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorTest.java b/src/test/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorTest.java new file mode 100644 index 000000000..19a6b1918 --- /dev/null +++ b/src/test/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorTest.java @@ -0,0 +1,94 @@ +package io.split.android.client.fallback; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class FallbackTreatmentsCalculatorTest { + + @Test + public void flagLevelOverrideTakesPrecedence() { + FallbackTreatment global = new FallbackTreatment("off", "{\"g\":true}"); + FallbackTreatment byFlag = new FallbackTreatment("on", "{\"f\":true}"); + Map map = new HashMap<>(); + map.put("my_flag", byFlag); + FallbackConfiguration config = FallbackConfiguration.builder() + .global(global) + .byFlag(map) + .build(); + + FallbackTreatmentsCalculator calculator = new FallbackTreatmentsCalculatorImpl(config); + FallbackTreatment resolvedExisting = calculator.resolve("my_flag"); + FallbackTreatment resolvedOther = calculator.resolve("other_flag"); + + assertNotNull(resolvedExisting); + assertEquals(byFlag, resolvedExisting); + assertNotNull(resolvedOther); + assertEquals(global, resolvedOther); + } + + @Test + public void globalFallbackIsReturnedWhenNoFlagOverride() { + FallbackTreatment global = new FallbackTreatment("off"); + FallbackConfiguration config = FallbackConfiguration.builder() + .global(global) + .byFlag(Collections.emptyMap()) + .build(); + + FallbackTreatmentsCalculator calculator = new FallbackTreatmentsCalculatorImpl(config); + FallbackTreatment resolved = calculator.resolve("any_flag"); + + assertNotNull(resolved); + assertEquals(global, resolved); + } + + @Test + public void flagLevelFallbackIsReturnedWhenConfigured() { + FallbackTreatment byFlag = new FallbackTreatment("on"); + Map map = new HashMap<>(); + map.put("flagA", byFlag); + FallbackConfiguration config = FallbackConfiguration.builder() + .byFlag(map) + .build(); + + FallbackTreatmentsCalculator calculator = new FallbackTreatmentsCalculatorImpl(config); + FallbackTreatment resolved = calculator.resolve("flagA"); + + assertNotNull(resolved); + assertEquals(byFlag, resolved); + } + + @Test + public void returnsControlWhenNoFallbackConfigured() { + FallbackConfiguration config = FallbackConfiguration.builder() + .build(); + + FallbackTreatmentsCalculator calculator = new FallbackTreatmentsCalculatorImpl(config); + FallbackTreatment resolved = calculator.resolve("nope"); + + assertNotNull(resolved); + assertEquals(FallbackTreatment.CONTROL, resolved); + } + + @Test + public void nonexistentFlagFallsBackToGlobal() { + FallbackTreatment global = new FallbackTreatment("off"); + Map map = new HashMap<>(); + map.put("flagA", new FallbackTreatment("on")); + FallbackConfiguration config = FallbackConfiguration.builder() + .global(global) + .byFlag(map) + .build(); + + FallbackTreatmentsCalculator calculator = new FallbackTreatmentsCalculatorImpl(config); + FallbackTreatment resolved = calculator.resolve("flagB"); + + assertNotNull(resolved); + assertEquals(global, resolved); + } +}