diff --git a/build.gradle b/build.gradle index 30f6c0d05..2e9b5b961 100644 --- a/build.gradle +++ b/build.gradle @@ -40,14 +40,17 @@ dependencies { } jar { - baseName = 'launchdarkly-client' - manifest { - attributes("Implementation-Version": version) - } + baseName = 'launchdarkly-client' + // thin classifier means that the non-shaded non-fat jar is still available + // but is opt-in since users will have to specify it. + classifier = 'thin' + manifest { + attributes("Implementation-Version": version) + } } task wrapper(type: Wrapper) { - gradleVersion = '2.0' + gradleVersion = '2.14-rc-3' } buildscript { @@ -90,7 +93,36 @@ githubPages { } shadowJar { - classifier = 'all' + baseName = 'launchdarkly-client' + //no classifier means that the fat + shaded jar becomes the default artifact + classifier = '' + + // Shade all jars except for launchdarkly + relocate('com', 'com.launchdarkly.shaded.com') { + exclude("com.launchdarkly.client.*") + exclude("com.launchdarkly.eventsource.*") + exclude("com.google.gson.*") + exclude("com.google.gson.annotations.*") + exclude("com.google.gson.internal.*") + exclude("com.google.gson.internal.bind.*") + exclude("com.google.gson.internal.bind.util.*") + exclude("com.google.gson.internal.bind.util.*") + exclude("com.google.gson.reflect.*") + exclude("com.google.gson.stream.*") + } + relocate('okhttp3', 'com.launchdarkly.shaded.okhttp3') + relocate('okio', 'com.launchdarkly.shaded.okio') + relocate('org', 'com.launchdarkly.shaded.org') { + exclude("org.slf4j.*") + exclude("org.slf4j.event.*") + exclude("org.slf4j.helpers.*") + exclude("org.slf4j.spi.*") + } + relocate('redis', 'com.launchdarkly.shaded.redis') + + manifest { + attributes("Implementation-Version": version) + } } test { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index b76121670..3baa6058a 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3689250db..24b15b161 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Aug 11 16:15:27 PDT 2014 +#Tue Jun 07 10:02:02 PDT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-rc-3-all.zip diff --git a/gradlew b/gradlew index 91a7e269e..27309d923 100755 --- a/gradlew +++ b/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +113,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/gradlew.bat b/gradlew.bat index aec99730b..f6d5974e7 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/src/main/java/com/launchdarkly/client/Clause.java b/src/main/java/com/launchdarkly/client/Clause.java index 335549d25..53c315bb5 100644 --- a/src/main/java/com/launchdarkly/client/Clause.java +++ b/src/main/java/com/launchdarkly/client/Clause.java @@ -17,7 +17,7 @@ class Clause { private boolean negate; boolean matchesUser(LDUser user) { - JsonElement userValue = valueOf(user, attribute); + JsonElement userValue = user.getValueForEvaluation(attribute); if (userValue == null) { return false; } @@ -58,27 +58,5 @@ private boolean maybeNegate(boolean b) { return b; } - static JsonElement valueOf(LDUser user, String attribute) { - switch (attribute) { - case "key": - return user.getKey(); - case "ip": - return user.getIp(); - case "country": - return user.getCountry(); - case "email": - return user.getEmail(); - case "firstName": - return user.getFirstName(); - case "lastName": - return user.getLastName(); - case "avatar": - return user.getAvatar(); - case "name": - return user.getName(); - case "anonymous": - return user.getAnonymous(); - } - return user.getCustom(attribute); - } + } diff --git a/src/main/java/com/launchdarkly/client/FeatureFlag.java b/src/main/java/com/launchdarkly/client/FeatureFlag.java index 6488b5372..40f708113 100644 --- a/src/main/java/com/launchdarkly/client/FeatureFlag.java +++ b/src/main/java/com/launchdarkly/client/FeatureFlag.java @@ -23,7 +23,7 @@ class FeatureFlag { private final String salt; private final List targets; private final List rules; - private final Rule fallthrough; + private final VariationOrRollout fallthrough; private final Integer offVariation; //optional private final List variations; private final boolean deleted; @@ -36,7 +36,7 @@ static Map fromJsonMap(String json) { return gson.fromJson(json, mapType); } - FeatureFlag(String key, int version, boolean on, List prerequisites, String salt, List targets, List rules, Rule fallthrough, Integer offVariation, List variations, boolean deleted) { + FeatureFlag(String key, int version, boolean on, List prerequisites, String salt, List targets, List rules, VariationOrRollout fallthrough, Integer offVariation, List variations, boolean deleted) { this.key = key; this.version = version; this.on = on; @@ -70,36 +70,37 @@ EvalResult evaluate(LDUser user, FeatureStore featureStore) { return evaluate(user, featureStore, prereqEvents, visited); } + // Returning either a nil EvalResult or EvalResult.value indicates prereq failure/error. private EvalResult evaluate(LDUser user, FeatureStore featureStore, List events, Set visited) { + boolean prereqOk = true; + EvalResult evalResult = new EvalResult(null, events, visited); for (Prerequisite prereq : prerequisites) { - visited.add(key); - if (visited.contains(prereq.getKey())) { + evalResult.visitedFeatureKeys.add(key); + if (evalResult.visitedFeatureKeys.contains(prereq.getKey())) { logger.error("Prerequisite cycle detected when evaluating feature flag: " + key); return null; } + JsonElement prereqEvalResultValue = null; FeatureFlag prereqFeatureFlag = featureStore.get(prereq.getKey()); if (prereqFeatureFlag == null) { logger.error("Could not retrieve prerequisite flag: " + prereq.getKey() + " when evaluating: " + key); return null; - } - JsonElement prereqValue; - if (prereqFeatureFlag.isOn()) { - EvalResult prereqEvalResult = prereqFeatureFlag.evaluate(user, featureStore, events, visited); - if (prereqEvalResult == null) { - return null; - } - prereqValue = prereqEvalResult.value; - visited = prereqEvalResult.visitedFeatureKeys; - events = prereqEvalResult.prerequisiteEvents; - events.add(new FeatureRequestEvent(prereqFeatureFlag.getKey(), user, prereqValue, null)); - if (prereqValue == null || !prereqValue.equals(prereqFeatureFlag.getVariation(prereq.getVariation()))) { - return new EvalResult(null, events, visited); + } else if (prereqFeatureFlag.isOn()) { + EvalResult prereqEvalResult = prereqFeatureFlag.evaluate(user, featureStore, evalResult.prerequisiteEvents, evalResult.visitedFeatureKeys); + if (prereqEvalResult == null || prereqEvalResult.getValue() == null || !prereqEvalResult.value.equals(prereqFeatureFlag.getVariation(prereq.getVariation()))) { + prereqOk = false; } + prereqEvalResultValue = prereqEvalResult != null ? prereqEvalResult.getValue() : null; } else { - return null; + prereqOk = false; } + //We don't short circuit and also send events for each prereq. + evalResult.prerequisiteEvents.add(new FeatureRequestEvent(prereqFeatureFlag.getKey(), user, prereqEvalResultValue, null)); + } + if (prereqOk) { + evalResult.value = getVariation(evaluateIndex(user)); } - return new EvalResult(getVariation(evaluateIndex(user)), events, visited); + return evalResult; } private Integer evaluateIndex(LDUser user) { @@ -163,7 +164,7 @@ List getRules() { return rules; } - Rule getFallthrough() { + VariationOrRollout getFallthrough() { return fallthrough; } diff --git a/src/main/java/com/launchdarkly/client/FeatureFlagBuilder.java b/src/main/java/com/launchdarkly/client/FeatureFlagBuilder.java index 0369fd735..120764862 100644 --- a/src/main/java/com/launchdarkly/client/FeatureFlagBuilder.java +++ b/src/main/java/com/launchdarkly/client/FeatureFlagBuilder.java @@ -2,19 +2,20 @@ import com.google.gson.JsonElement; +import java.util.ArrayList; import java.util.List; class FeatureFlagBuilder { private String key; private int version; private boolean on; - private List prerequisites; + private List prerequisites = new ArrayList<>(); private String salt; - private List targets; - private List rules; - private Rule fallthrough; + private List targets = new ArrayList<>(); + private List rules = new ArrayList<>(); + private VariationOrRollout fallthrough; private Integer offVariation; - private List variations; + private List variations = new ArrayList<>(); private boolean deleted; FeatureFlagBuilder(String key) { @@ -68,7 +69,7 @@ FeatureFlagBuilder rules(List rules) { return this; } - FeatureFlagBuilder fallthrough(Rule fallthrough) { + FeatureFlagBuilder fallthrough(VariationOrRollout fallthrough) { this.fallthrough = fallthrough; return this; } diff --git a/src/main/java/com/launchdarkly/client/LDUser.java b/src/main/java/com/launchdarkly/client/LDUser.java index c10404f9d..270d9a978 100644 --- a/src/main/java/com/launchdarkly/client/LDUser.java +++ b/src/main/java/com/launchdarkly/client/LDUser.java @@ -64,6 +64,14 @@ public LDUser(String key) { this.custom = new HashMap<>(); } + protected JsonElement getValueForEvaluation(String attribute) { + try { + return UserAttribute.valueOf(attribute).get(this); + } catch (IllegalArgumentException expected) { + return getCustom(attribute); + } + } + JsonPrimitive getKey() { return key; } diff --git a/src/main/java/com/launchdarkly/client/Operator.java b/src/main/java/com/launchdarkly/client/Operator.java index d2112b968..980e32a84 100644 --- a/src/main/java/com/launchdarkly/client/Operator.java +++ b/src/main/java/com/launchdarkly/client/Operator.java @@ -14,6 +14,10 @@ enum Operator { in { @Override public boolean apply(JsonPrimitive uValue, JsonPrimitive cValue) { + if (uValue.equals(cValue)) { + return true; + } + if (uValue.isString() && cValue.isString()) { if (uValue.getAsString().equals(cValue.getAsString())) return true; diff --git a/src/main/java/com/launchdarkly/client/Prerequisite.java b/src/main/java/com/launchdarkly/client/Prerequisite.java index 99004a7e1..d934b7c22 100644 --- a/src/main/java/com/launchdarkly/client/Prerequisite.java +++ b/src/main/java/com/launchdarkly/client/Prerequisite.java @@ -1,8 +1,13 @@ package com.launchdarkly.client; class Prerequisite { - private String key; - private int variation; + private final String key; + private final int variation; + + Prerequisite(String key, int variation) { + this.key = key; + this.variation = variation; + } String getKey() { return key; diff --git a/src/main/java/com/launchdarkly/client/Rule.java b/src/main/java/com/launchdarkly/client/Rule.java index f0dcd2610..b2095452d 100644 --- a/src/main/java/com/launchdarkly/client/Rule.java +++ b/src/main/java/com/launchdarkly/client/Rule.java @@ -1,28 +1,18 @@ package com.launchdarkly.client; -import com.google.gson.JsonElement; -import org.apache.commons.codec.digest.DigestUtils; - import java.util.List; -import static com.launchdarkly.client.Clause.valueOf; - /** * Expresses a set of AND-ed matching conditions for a user, along with either the fixed variation or percent rollout * to serve if the conditions match. * Invariant: one of the variation or rollout must be non-nil. */ -class Rule { - private static final float long_scale = (float) 0xFFFFFFFFFFFFFFFL; - - private List clauses; - private Integer variation; - private Rollout rollout; +class Rule extends VariationOrRollout { + private final List clauses; Rule(List clauses, Integer variation, Rollout rollout) { + super(variation, rollout); this.clauses = clauses; - this.variation = variation; - this.rollout = rollout; } boolean matchesUser(LDUser user) { @@ -33,58 +23,4 @@ boolean matchesUser(LDUser user) { } return true; } - - Integer variationIndexForUser(LDUser user, String key, String salt) { - if (variation != null) { - return variation; - } else if (rollout != null) { - String bucketBy = rollout.bucketBy == null ? "key" : rollout.bucketBy; - Float bucket = bucketUser(user, key, bucketBy, salt); - Float sum = 0F; - for (WeightedVariation wv : rollout.variations) { - sum += (float)wv.weight / 100000F; - if (bucket < sum) { - return wv.variation; - } - } - } - return null; - } - - Float bucketUser(LDUser user, String key, String attr, String salt) { - JsonElement userValue = valueOf(user, attr); - String idHash; - if (userValue != null) { - if (userValue.isJsonPrimitive() && userValue.getAsJsonPrimitive().isString()) { - idHash = userValue.getAsString(); - if (user.getSecondary() != null) { - idHash = idHash + "." + user.getSecondary().getAsString(); - } - String hash = DigestUtils.sha1Hex(key + "." + salt + "." + idHash).substring(0, 15); - long longVal = Long.parseLong(hash, 16); - return (float) longVal / long_scale; - } - } - return null; - } - - static class Rollout { - private List variations; - private String bucketBy; - - public Rollout(List variations, String bucketBy) { - this.variations = variations; - this.bucketBy = bucketBy; - } - } - - static class WeightedVariation { - private int variation; - private int weight; - - public WeightedVariation(int variation, int weight) { - this.variation = variation; - this.weight = weight; - } - } } diff --git a/src/main/java/com/launchdarkly/client/Target.java b/src/main/java/com/launchdarkly/client/Target.java index 3cd7cab0d..a6c5a648e 100644 --- a/src/main/java/com/launchdarkly/client/Target.java +++ b/src/main/java/com/launchdarkly/client/Target.java @@ -3,8 +3,13 @@ import java.util.List; class Target { - private List values; - private int variation; + private final List values; + private final int variation; + + Target(List values, int variation) { + this.values = values; + this.variation = variation; + } List getValues() { return values; diff --git a/src/main/java/com/launchdarkly/client/UserAttribute.java b/src/main/java/com/launchdarkly/client/UserAttribute.java index 8b024e741..40f3ce44f 100644 --- a/src/main/java/com/launchdarkly/client/UserAttribute.java +++ b/src/main/java/com/launchdarkly/client/UserAttribute.java @@ -1,14 +1,64 @@ package com.launchdarkly.client; +import com.google.gson.JsonElement; + enum UserAttribute { - key, - secondary, - ip, - email, - name, - avatar, - firstName, - lastName, - anonymous, - country + key { + JsonElement get(LDUser user) { + return user.getKey(); + } + }, + secondary { + JsonElement get(LDUser user) { + return null; //Not used for evaluation. + } + }, + ip { + JsonElement get(LDUser user) { + return user.getIp(); + } + }, + email { + JsonElement get(LDUser user) { + return user.getEmail(); + } + }, + avatar { + JsonElement get(LDUser user) { + return user.getAvatar(); + } + }, + firstName { + JsonElement get(LDUser user) { + return user.getFirstName(); + } + }, + lastName { + JsonElement get(LDUser user) { + return user.getLastName(); + } + }, + name { + JsonElement get(LDUser user) { + return user.getName(); + } + }, + country { + JsonElement get(LDUser user) { + return user.getCountry(); + } + }, + anonymous { + JsonElement get(LDUser user) { + return user.getAnonymous(); + } + }; + + /** + * Gets value for Rule evaluation for a user. + * + * @param user + * @return + */ + abstract JsonElement get(LDUser user); } diff --git a/src/main/java/com/launchdarkly/client/VariationOrRollout.java b/src/main/java/com/launchdarkly/client/VariationOrRollout.java new file mode 100644 index 000000000..c1696c8ae --- /dev/null +++ b/src/main/java/com/launchdarkly/client/VariationOrRollout.java @@ -0,0 +1,77 @@ +package com.launchdarkly.client; + + +import com.google.gson.JsonElement; +import org.apache.commons.codec.digest.DigestUtils; + +import java.util.List; + +/** + * Contains either a fixed variation or percent rollout to serve. + * Invariant: one of the variation or rollout must be non-nil. + */ +class VariationOrRollout { + private static final float long_scale = (float) 0xFFFFFFFFFFFFFFFL; + + private final Integer variation; + private final Rollout rollout; + + VariationOrRollout(Integer variation, Rollout rollout) { + this.variation = variation; + this.rollout = rollout; + } + + Integer variationIndexForUser(LDUser user, String key, String salt) { + if (variation != null) { + return variation; + } else if (rollout != null) { + String bucketBy = rollout.bucketBy == null ? "key" : rollout.bucketBy; + float bucket = bucketUser(user, key, bucketBy, salt); + float sum = 0F; + for (WeightedVariation wv : rollout.variations) { + sum += (float) wv.weight / 100000F; + if (bucket < sum) { + return wv.variation; + } + } + } + return null; + } + + private float bucketUser(LDUser user, String key, String attr, String salt) { + JsonElement userValue = user.getValueForEvaluation(attr); + String idHash; + if (userValue != null) { + if (userValue.isJsonPrimitive() && userValue.getAsJsonPrimitive().isString()) { + idHash = userValue.getAsString(); + if (user.getSecondary() != null) { + idHash = idHash + "." + user.getSecondary().getAsString(); + } + String hash = DigestUtils.sha1Hex(key + "." + salt + "." + idHash).substring(0, 15); + long longVal = Long.parseLong(hash, 16); + return (float) longVal / long_scale; + } + } + return 0F; + } + + static class Rollout { + private final List variations; + private final String bucketBy; + + Rollout(List variations, String bucketBy) { + this.variations = variations; + this.bucketBy = bucketBy; + } + } + + static class WeightedVariation { + private final int variation; + private final int weight; + + WeightedVariation(int variation, int weight) { + this.variation = variation; + this.weight = weight; + } + } +} diff --git a/src/test/java/com/launchdarkly/client/FeatureFlagTest.java b/src/test/java/com/launchdarkly/client/FeatureFlagTest.java new file mode 100644 index 000000000..e12a0b1bd --- /dev/null +++ b/src/test/java/com/launchdarkly/client/FeatureFlagTest.java @@ -0,0 +1,123 @@ +package com.launchdarkly.client; + + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; + +import static java.util.Collections.singletonList; + +public class FeatureFlagTest { + + private FeatureStore featureStore; + + @Before + public void before() { + featureStore = new InMemoryFeatureStore(); + } + + @Test + public void testPrereqSelfCycle() { + String keyA = "keyA"; + FeatureFlag f = newFlagWithPrereq(keyA, keyA); + + featureStore.upsert(keyA, f); + LDUser user = new LDUser.Builder("userKey").build(); + Assert.assertNull(f.evaluate(user, featureStore)); + } + + @Test + public void testPrereqSimpleCycle() { + String keyA = "keyA"; + String keyB = "keyB"; + FeatureFlag f1 = newFlagWithPrereq(keyA, keyB); + FeatureFlag f2 = newFlagWithPrereq(keyB, keyA); + + featureStore.upsert(f1.getKey(), f1); + featureStore.upsert(f2.getKey(), f2); + LDUser user = new LDUser.Builder("userKey").build(); + Assert.assertNull(f1.evaluate(user, featureStore).getValue()); + Assert.assertNull(f2.evaluate(user, featureStore).getValue()); + } + + @Test + public void testPrereqCycle() { + String keyA = "keyA"; + String keyB = "keyB"; + String keyC = "keyC"; + FeatureFlag f1 = newFlagWithPrereq(keyA, keyB); + FeatureFlag f2 = newFlagWithPrereq(keyB, keyC); + FeatureFlag f3 = newFlagWithPrereq(keyC, keyA); + + featureStore.upsert(f1.getKey(), f1); + featureStore.upsert(f2.getKey(), f2); + featureStore.upsert(f3.getKey(), f3); + LDUser user = new LDUser.Builder("userKey").build(); + Assert.assertNull(f1.evaluate(user, featureStore).getValue()); + Assert.assertNull(f2.evaluate(user, featureStore).getValue()); + Assert.assertNull(f3.evaluate(user, featureStore).getValue()); + } + + @Test + public void testPrereqDoesNotExist() { + String keyA = "keyA"; + String keyB = "keyB"; + FeatureFlag f1 = newFlagWithPrereq(keyA, keyB); + + featureStore.upsert(f1.getKey(), f1); + LDUser user = new LDUser.Builder("userKey").build(); + Assert.assertNull(f1.evaluate(user, featureStore)); + } + + @Test + public void testPrereqCollectsEventsForPrereqs() { + String keyA = "keyA"; + String keyB = "keyB"; + String keyC = "keyC"; + FeatureFlag flagA = newFlagWithPrereq(keyA, keyB); + FeatureFlag flagB = newFlagWithPrereq(keyB, keyC); + FeatureFlag flagC = newFlagOff(keyC); + + featureStore.upsert(flagA.getKey(), flagA); + featureStore.upsert(flagB.getKey(), flagB); + featureStore.upsert(flagC.getKey(), flagC); + + LDUser user = new LDUser.Builder("userKey").build(); + + FeatureFlag.EvalResult flagAResult = flagA.evaluate(user, featureStore); + Assert.assertNotNull(flagAResult); + Assert.assertNull(flagAResult.getValue()); + Assert.assertEquals(2, flagAResult.getPrerequisiteEvents().size()); + + FeatureFlag.EvalResult flagBResult = flagB.evaluate(user, featureStore); + Assert.assertNotNull(flagBResult); + Assert.assertNull(flagBResult.getValue()); + Assert.assertEquals(1, flagBResult.getPrerequisiteEvents().size()); + + FeatureFlag.EvalResult flagCResult = flagC.evaluate(user, featureStore); + Assert.assertNotNull(flagCResult); + Assert.assertEquals(new JsonPrimitive(0), flagCResult.getValue()); + Assert.assertEquals(0, flagCResult.getPrerequisiteEvents().size()); + } + + private FeatureFlag newFlagWithPrereq(String featureKey, String prereqKey) { + return new FeatureFlagBuilder(featureKey) + .prerequisites(singletonList(new Prerequisite(prereqKey, 0))) + .variations(Arrays.asList(new JsonPrimitive(0), new JsonPrimitive(1))) + .fallthrough(new VariationOrRollout(0, null)) + .on(true) + .build(); + } + + private FeatureFlag newFlagOff(String featureKey) { + return new FeatureFlagBuilder(featureKey) + .variations(Arrays.asList(new JsonPrimitive(0), new JsonPrimitive(1))) + .fallthrough(new VariationOrRollout(0, null)) + .on(false) + .build(); + } +}