Skip to content

Commit

Permalink
Add semver support (FF-1573) (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
leoromanovsky committed Mar 8, 2024
1 parent 896716d commit 04fe3ed
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 24 deletions.
1 change: 1 addition & 0 deletions eppo/build.gradle
Expand Up @@ -71,6 +71,7 @@ dependencies {
androidTestImplementation "commons-io:commons-io:${versions.commonsio}"
implementation("com.google.code.gson:gson:${versions.gson}")
implementation("com.squareup.okhttp3:okhttp:${versions.okhttp}")
implementation("com.github.zafarkhaja:java-semver:0.10.2")
}

publishing {
Expand Down
Expand Up @@ -262,7 +262,7 @@ public void testCachedAssignments() {

// wait for a bit since cache file is loaded asynchronously
System.out.println("Sleeping for a bit to wait for cache population to complete");
Thread.sleep(5000);
Thread.sleep(10000);

// Then reinitialize with a bad host so we know it's using the cached RAC built from the first initialization
initClient(INVALID_HOST, false, false, false); // invalid port to force to use cache
Expand Down
76 changes: 62 additions & 14 deletions eppo/src/main/java/cloud/eppo/android/RuleEvaluator.java
@@ -1,24 +1,17 @@
package cloud.eppo.android;

import com.github.zafarkhaja.semver.Version;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import cloud.eppo.android.dto.EppoValue;
import cloud.eppo.android.dto.SubjectAttributes;
import cloud.eppo.android.dto.TargetingCondition;
import cloud.eppo.android.dto.TargetingRule;

interface IConditionFunc<T> {
boolean check(T a, T b);
}

class Compare {
public static boolean compareNumber(double a, double b, IConditionFunc<Double> conditionFunc) {
return conditionFunc.check(a, b);
}

public static boolean compareRegex(String a, Pattern pattern) {
return pattern.matcher(a).matches();
}
Expand Down Expand Up @@ -52,17 +45,72 @@ private static boolean evaluateCondition(SubjectAttributes subjectAttributes, Ta
) {
if (subjectAttributes.containsKey(condition.getAttribute())) {
EppoValue value = subjectAttributes.get(condition.getAttribute());
if (value == null) {
return false;
}

boolean numericComparison = value.isNumeric() && condition.getValue().isNumeric();

// Android API version 21 does not have access to the java.util.Optional class.
// Version.tryParse returns a Optional<Version> would be ideal.
// Instead use Version.parse which throws an exception if the string is not a valid SemVer.
// We front-load the parsing here so many evaluation of gte, gt, lte, lt operations
// more straight-forward.
Version valueSemVer = null;
Version conditionSemVer = null;
try {
valueSemVer = Version.parse(value.stringValue());
conditionSemVer = Version.parse(condition.getValue().stringValue());
} catch (Exception e) {
// no-op
}

// Performing this check satisfies the compiler that the possibly
// null value can be safely accessed later.
boolean semVerComparison = valueSemVer != null && conditionSemVer != null;

try {
switch (condition.getOperator()) {
case GreaterThanEqualTo:
return Compare.compareNumber(value.doubleValue(), condition.getValue().doubleValue()
, (a, b) -> a >= b);
if (numericComparison) {
return value.doubleValue() >= condition.getValue().doubleValue();
}

if (semVerComparison) {
return valueSemVer.isHigherThanOrEquivalentTo(conditionSemVer);
}

return false;
case GreaterThan:
return Compare.compareNumber(value.doubleValue(), condition.getValue().doubleValue(), (a, b) -> a > b);
if (numericComparison) {
return value.doubleValue() > condition.getValue().doubleValue();
}

if (semVerComparison) {
return valueSemVer.isHigherThan(conditionSemVer);
}

return false;
case LessThanEqualTo:
return Compare.compareNumber(value.doubleValue(), condition.getValue().doubleValue(), (a, b) -> a <= b);
if (numericComparison) {
return value.doubleValue() <= condition.getValue().doubleValue();
}

if (semVerComparison) {
return valueSemVer.isLowerThanOrEquivalentTo(conditionSemVer);
}

return false;
case LessThan:
return Compare.compareNumber(value.doubleValue(), condition.getValue().doubleValue(), (a, b) -> a < b);
if (numericComparison) {
return value.doubleValue() < condition.getValue().doubleValue();
}

if (semVerComparison) {
return valueSemVer.isLowerThan(conditionSemVer);
}

return false;
case Matches:
return Compare.compareRegex(value.stringValue(), Pattern.compile(condition.getValue().stringValue()));
case OneOf:
Expand Down
10 changes: 1 addition & 9 deletions eppo/src/main/java/cloud/eppo/android/dto/EppoValue.java
Expand Up @@ -57,18 +57,10 @@ public static EppoValue valueOf() {
return new EppoValue(EppoValueType.Null);
}

public int intValue() {
return Integer.parseInt(value, 10);
}

public double doubleValue() {
return Double.parseDouble(value);
}

public long longValue() {
return Long.parseLong(value, 10);
}

public String stringValue() {
return value;
}
Expand All @@ -87,7 +79,7 @@ public JsonElement jsonValue() {

public boolean isNumeric() {
try {
Long.parseLong(value, 10);
Double.parseDouble(value);
return true;
} catch (Exception e) {
return false;
Expand Down
27 changes: 27 additions & 0 deletions eppo/src/test/java/cloud/eppo/android/RuleEvaluatorTest.java
Expand Up @@ -43,6 +43,21 @@ public void addNumericConditionToRule(TargetingRule TargetingRule) {
addConditionToRule(TargetingRule, condition2);
}

public void addSemVerConditionToRule(TargetingRule TargetingRule) {
TargetingCondition condition1 = new TargetingCondition();
condition1.setValue(EppoValue.valueOf("1.5.0"));
condition1.setAttribute("appVersion");
condition1.setOperator(OperatorType.GreaterThanEqualTo);

TargetingCondition condition2 = new TargetingCondition();
condition2.setValue(EppoValue.valueOf("2.2.0"));
condition2.setAttribute("appVersion");
condition2.setOperator(OperatorType.LessThan);

addConditionToRule(TargetingRule, condition1);
addConditionToRule(TargetingRule, condition2);
}

public void addRegexConditionToRule(TargetingRule TargetingRule) {
TargetingCondition condition = new TargetingCondition();
condition.setValue(EppoValue.valueOf("[a-z]+"));
Expand Down Expand Up @@ -131,6 +146,18 @@ public void testMatchesAnyRuleWhenRuleMatches() {
assertEquals(targetingRule, RuleEvaluator.findMatchingRule(subjectAttributes, targetingRules));
}

@Test
public void testMatchesAnyRuleWhenRuleMatchesWithSemVer() {
List<TargetingRule> targetingRules = new ArrayList<>();
TargetingRule targetingRule = createRule(new ArrayList<>());
addSemVerConditionToRule(targetingRule);
targetingRules.add(targetingRule);

SubjectAttributes subjectAttributes = new SubjectAttributes();
subjectAttributes.put("appVersion", "1.15.5");

assertEquals(targetingRule, RuleEvaluator.findMatchingRule(subjectAttributes, targetingRules));
}

@Test
public void testMatchesAnyRuleWhenThrowInvalidSubjectAttribute() {
Expand Down

0 comments on commit 04fe3ed

Please sign in to comment.